mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-14 08:35:29 +08:00
Spruce up the dashboard
Consloidate modal functionality into its own component and make general appearance fixes, especially with the Syncback Request Details.
This commit is contained in:
parent
3e8623d383
commit
8e790a0e15
9 changed files with 251 additions and 159 deletions
|
@ -51,14 +51,16 @@ pre {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
#set-all-sync {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
#open-all-sync {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.right-action {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
display: inline-block;
|
||||
color: rgba(16, 83, 161, 0.88);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
@ -66,7 +68,7 @@ pre {
|
|||
}
|
||||
|
||||
.action-link.cancel {
|
||||
margin-left: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sync-policy textarea {
|
||||
|
@ -78,8 +80,9 @@ pre {
|
|||
.modal {
|
||||
background-color: white;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
margin: 10vh auto;
|
||||
padding: 20px;
|
||||
max-height: calc(80vh - 40px); /* minus padding */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
@ -90,16 +93,27 @@ pre {
|
|||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding-top: 10%;
|
||||
}
|
||||
|
||||
.modal-close-wrapper {
|
||||
position: relative;
|
||||
height: 0;
|
||||
width: 0;
|
||||
float: right;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: relative;
|
||||
float: right;
|
||||
top: -10px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
background: url('../images/close.png') center center no-repeat;
|
||||
background-size: 12px auto;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.sync-graph {
|
||||
|
@ -127,6 +141,10 @@ pre {
|
|||
|
||||
#syncback-request-details table {
|
||||
width: 100%;
|
||||
border: solid black 1px;
|
||||
box-shadow: 1px 1px #333333;
|
||||
margin: 10px 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#syncback-request-details tr:nth-child(even) {
|
||||
|
@ -140,6 +158,7 @@ pre {
|
|||
#syncback-request-details td, #syncback-request-details th {
|
||||
text-align: center;
|
||||
padding: 10px 5px;
|
||||
border: solid black 1px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
|
@ -153,9 +172,11 @@ pre {
|
|||
position: absolute;
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
position: relative;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
|
@ -170,6 +191,5 @@ pre {
|
|||
.dropdown-wrapper {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
|
BIN
packages/nylas-dashboard/public/images/close.png
Normal file
BIN
packages/nylas-dashboard/public/images/close.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -3,6 +3,7 @@
|
|||
<script src="/js/react.js"></script>
|
||||
<script src="/js/react-dom.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
|
||||
<script src="/js/modal.jsx" type="text/babel"></script>
|
||||
<script src="/js/sync-policy.jsx" type="text/babel"></script>
|
||||
<script src="/js/set-all-sync-policies.jsx" type="text/babel"></script>
|
||||
<script src="/js/account-filter.jsx" type="text/babel"></script>
|
||||
|
|
|
@ -45,7 +45,7 @@ class Account extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<div className="section">Error</div>
|
||||
<span className="action-link" onClick={() => this.clearError()}>Clear Error</span>
|
||||
<div className="action-link" onClick={() => this.clearError()}>Clear Error</div>
|
||||
<div className="error">
|
||||
<pre>
|
||||
{JSON.stringify(error, null, 2)}
|
||||
|
|
|
@ -38,7 +38,7 @@ class Dropdown extends React.Component {
|
|||
|
||||
// All options, not shown if dropdown is closed
|
||||
let options = [];
|
||||
let optionsWrapper = <span />;
|
||||
let optionsWrapper = <span className="dropdown-options" />;
|
||||
if (!this.state.closed) {
|
||||
for (const opt of this.props.options) {
|
||||
options.push(
|
||||
|
@ -54,8 +54,8 @@ class Dropdown extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="dropdown-wrapper" tabIndex="0" onBlur={() => this.close.call(this)}>
|
||||
{selected}
|
||||
{optionsWrapper}
|
||||
{selected}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
102
packages/nylas-dashboard/public/js/modal.jsx
Normal file
102
packages/nylas-dashboard/public/js/modal.jsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
const React = window.React;
|
||||
|
||||
class Modal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
onOpen: props.onOpen || () => {},
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.keydownHandler = (e) => {
|
||||
// Close modal on escape
|
||||
if (e.keyCode === 27) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', this.keydownHandler);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.keydownHandler);
|
||||
}
|
||||
|
||||
open() {
|
||||
this.setState({open: true});
|
||||
this.state.onOpen();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.setState({open: false});
|
||||
}
|
||||
|
||||
// type can be 'button' or 'div'.
|
||||
// Always closes modal after the callback
|
||||
renderActionElem({title, type = 'button', action = () => {}, className = ""}) {
|
||||
const callback = (e) => {
|
||||
action(e);
|
||||
this.close();
|
||||
}
|
||||
if (type === 'button') {
|
||||
return (
|
||||
<button className={className} onClick={callback}>
|
||||
{title}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={className} onClick={callback}>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const activator = (
|
||||
<div
|
||||
className={this.props.openLink.className}
|
||||
id={this.props.openLink.id}
|
||||
onClick={() => this.open.call(this)}
|
||||
>
|
||||
{this.props.openLink.text}
|
||||
</div>
|
||||
)
|
||||
if (!this.state.open) {
|
||||
return activator;
|
||||
}
|
||||
|
||||
const actionElems = [];
|
||||
if (this.props.actionElems) {
|
||||
for (const config of this.props.actionElems) {
|
||||
actionElems.push(this.renderActionElem(config));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{activator}
|
||||
<div className="modal-bg">
|
||||
<div className={`${this.props.className || ''} modal`} id={this.props.id}>
|
||||
<div className="modal-close-wrapper">
|
||||
<div className="modal-close" onClick={() => this.close.call(this)}></div>
|
||||
</div>
|
||||
{this.props.children}
|
||||
{actionElems}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
openLink: React.PropTypes.object,
|
||||
className: React.PropTypes.string,
|
||||
id: React.PropTypes.string,
|
||||
onOpen: React.PropTypes.func,
|
||||
actionElems: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
}
|
||||
|
||||
window.Modal = Modal;
|
|
@ -1,14 +1,7 @@
|
|||
const React = window.React;
|
||||
const Modal = window.Modal;
|
||||
|
||||
class SetAllSyncPolicies extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {editMode: false};
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.setState({editMode: true})
|
||||
}
|
||||
|
||||
applyToAllAccounts(accountIds) {
|
||||
const req = new XMLHttpRequest();
|
||||
|
@ -31,35 +24,31 @@ class SetAllSyncPolicies extends React.Component {
|
|||
}));
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.setState({editMode: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.editMode) {
|
||||
return (
|
||||
<div>
|
||||
<span className="action-link" id="set-all-sync" onClick={() => this.edit.call(this)}>
|
||||
Set sync policies for currently displayed accounts
|
||||
</span>
|
||||
<div className="modal-bg">
|
||||
<div className="sync-policy modal">
|
||||
<div className="section">Sync Policy</div>
|
||||
<textarea id="sync-policy-all">
|
||||
</textarea>
|
||||
<button onClick={() => this.applyToAllAccounts.call(this, this.props.accountIds)}>
|
||||
Apply To All Displayed Accounts
|
||||
</button>
|
||||
<span className="action-link cancel" onClick={() => this.cancel.call(this)}> Cancel </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="action-link" id="set-all-sync" onClick={() => this.edit.call(this)}>
|
||||
Set sync policies for currently displayed accounts
|
||||
</span>
|
||||
<Modal
|
||||
className="sync-policy"
|
||||
openLink={{
|
||||
text: "Set sync policies for currently displayed accounts",
|
||||
className: "action-link",
|
||||
id: "open-all-sync",
|
||||
}}
|
||||
actionElems={[
|
||||
{
|
||||
title: "Apply To All Displayed Accounts",
|
||||
action: () => this.applyToAllAccounts.call(this, this.props.accountIds),
|
||||
type: 'button',
|
||||
className: 'right-action',
|
||||
}, {
|
||||
title: "Cancel",
|
||||
type: 'div',
|
||||
className: 'action-link cancel',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h3>Sync Policy</h3>
|
||||
<textarea id="sync-policy-all"></textarea>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ class SyncPolicy extends React.Component {
|
|||
{this.props.stringifiedSyncPolicy}
|
||||
</textarea>
|
||||
<button onClick={() => this.save.call(this)}> Save </button>
|
||||
<span className="action-link cancel" onClick={() => this.cancel.call(this)}> Cancel </span>
|
||||
<div className="action-link cancel" onClick={() => this.cancel.call(this)}> Cancel </div>
|
||||
</div>
|
||||
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ class SyncPolicy extends React.Component {
|
|||
<div className="sync-policy">
|
||||
<div className="section">Sync Policy</div>
|
||||
<pre>{this.props.stringifiedSyncPolicy}</pre>
|
||||
<span className="action-link" onClick={() => this.edit.call(this)}> Edit </span>
|
||||
<div className="action-link" onClick={() => this.edit.call(this)}> Edit </div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
const React = window.React;
|
||||
const Dropdown = window.Dropdown;
|
||||
const {Dropdown, Modal} = window;
|
||||
|
||||
class SyncbackRequestDetails extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
accountId: props.accountId,
|
||||
syncbackRequests: null,
|
||||
counts: null,
|
||||
|
@ -50,117 +49,98 @@ class SyncbackRequestDetails extends React.Component {
|
|||
this.setState({statusFilter: statusFilter});
|
||||
}
|
||||
|
||||
open() {
|
||||
this.getDetails();
|
||||
this.getCounts();
|
||||
this.setState({open: true});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.setState({open: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.open) {
|
||||
let counts = <span> Of requests created in the last hour: ... </span>
|
||||
if (this.state.counts) {
|
||||
const total = this.state.counts.new + this.state.counts.failed
|
||||
+ this.state.counts.succeeded;
|
||||
if (total === 0) {
|
||||
counts = "No requests made in the last hour";
|
||||
} else {
|
||||
counts = (
|
||||
<div className="counts">
|
||||
Of requests created in the last hour:
|
||||
<span
|
||||
style={{color: 'rgb(222, 68, 68)'}}
|
||||
title={`${this.state.counts.failed} out of ${total}`}
|
||||
>
|
||||
{this.state.counts.failed / total * 100}% failed
|
||||
</span>
|
||||
<span
|
||||
style={{color: 'green'}}
|
||||
title={`${this.state.counts.succeeded} out of ${total}`}
|
||||
>
|
||||
{this.state.counts.succeeded / total * 100}% succeeded
|
||||
</span>
|
||||
<span
|
||||
style={{color: 'rgb(98, 98, 179)'}}
|
||||
title={`${this.state.counts.new} out of ${total}`}
|
||||
>
|
||||
{/* .new was throwing off my syntax higlighting, so ignoring linter*/}
|
||||
{this.state.counts['new'] / total * 100}% are still new
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let details = "Loading..."
|
||||
if (this.state.syncbackRequests) {
|
||||
let reqs = JSON.parse(this.state.syncbackRequests);
|
||||
if (this.state.statusFilter !== 'all') {
|
||||
reqs = reqs.filter((req) => req.status === this.state.statusFilter);
|
||||
}
|
||||
let rows = [];
|
||||
if (reqs.length === 0) {
|
||||
rows.push(<tr><td>No results</td><td>-</td><td>-</td></tr>);
|
||||
}
|
||||
for (let i = reqs.length - 1; i >= 0; i--) {
|
||||
const req = reqs[i];
|
||||
const date = new Date(req.createdAt);
|
||||
rows.push(<tr key={req.id} title={`id: ${req.id}`}>
|
||||
<td> {req.status} </td>
|
||||
<td> {req.type} </td>
|
||||
<td> {date.toLocaleTimeString()}, {date.toLocaleDateString()} </td>
|
||||
</tr>)
|
||||
}
|
||||
details = (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Status:
|
||||
<Dropdown
|
||||
options={['all', 'FAILED', 'NEW', 'SUCCEEDED']}
|
||||
defaultOption="all"
|
||||
onSelect={(status) => this.setStatusFilter.call(this, status)}
|
||||
/>
|
||||
</th>
|
||||
<th> Type </th>
|
||||
<th> Created At </th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="action-link">Syncback Request Details </span>
|
||||
<div className="modal-bg">
|
||||
<div className="modal">
|
||||
<div className="modal-close" onClick={() => this.close.call(this)}>
|
||||
X
|
||||
</div>
|
||||
<div id="syncback-request-details">
|
||||
{counts}
|
||||
{details}
|
||||
</div>
|
||||
</div>
|
||||
let counts = "Loading...";
|
||||
if (this.state.counts) {
|
||||
const total = this.state.counts.new + this.state.counts.failed
|
||||
+ this.state.counts.succeeded;
|
||||
if (total === 0) {
|
||||
counts = <div className="counts"> No requests made in the last hour </div>
|
||||
} else {
|
||||
counts = (
|
||||
<div className="counts">
|
||||
Of requests created in the last hour:
|
||||
<span
|
||||
style={{color: 'rgb(222, 68, 68)'}}
|
||||
title={`${this.state.counts.failed} out of ${total}`}
|
||||
>
|
||||
{this.state.counts.failed / total * 100}% failed
|
||||
</span>
|
||||
<span
|
||||
style={{color: 'green'}}
|
||||
title={`${this.state.counts.succeeded} out of ${total}`}
|
||||
>
|
||||
{this.state.counts.succeeded / total * 100}% succeeded
|
||||
</span>
|
||||
<span
|
||||
style={{color: 'rgb(98, 98, 179)'}}
|
||||
title={`${this.state.counts.new} out of ${total}`}
|
||||
>
|
||||
{/* .new was throwing off my syntax higlighting, so ignoring linter*/}
|
||||
{this.state.counts['new'] / total * 100}% are still new
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let details = "Loading..."
|
||||
if (this.state.syncbackRequests) {
|
||||
let reqs = JSON.parse(this.state.syncbackRequests);
|
||||
if (this.state.statusFilter !== 'all') {
|
||||
reqs = reqs.filter((req) => req.status === this.state.statusFilter);
|
||||
}
|
||||
let rows = [];
|
||||
if (reqs.length === 0) {
|
||||
rows.push(<tr><td>No results</td><td>-</td><td>-</td></tr>);
|
||||
}
|
||||
for (let i = reqs.length - 1; i >= 0; i--) {
|
||||
const req = reqs[i];
|
||||
const date = new Date(req.createdAt);
|
||||
rows.push(<tr key={req.id} title={`id: ${req.id}`}>
|
||||
<td> {req.status} </td>
|
||||
<td> {req.type} </td>
|
||||
<td> {date.toLocaleTimeString()}, {date.toLocaleDateString()} </td>
|
||||
</tr>)
|
||||
}
|
||||
details = (
|
||||
<table><tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Status:
|
||||
<Dropdown
|
||||
options={['all', 'FAILED', 'NEW', 'SUCCEEDED']}
|
||||
defaultOption="all"
|
||||
onSelect={(status) => this.setStatusFilter.call(this, status)}
|
||||
/>
|
||||
</th>
|
||||
<th> Type </th>
|
||||
<th> Created At </th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody></table>
|
||||
);
|
||||
}
|
||||
// else, the modal isn't open
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="action-link" onClick={() => this.open.call(this)}>
|
||||
Syncback Request Details
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
<Modal
|
||||
id="syncback-request-details"
|
||||
openLink={{
|
||||
text: "Syncback Request Details",
|
||||
className: "action-link",
|
||||
}}
|
||||
onOpen={() => {
|
||||
this.getDetails();
|
||||
this.getCounts();
|
||||
}}
|
||||
>
|
||||
<h3>Recent Stats</h3>
|
||||
{counts}
|
||||
<br />
|
||||
<h3>Stored Syncback Requests</h3>
|
||||
{details}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue