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:
Halla Moore 2016-07-12 17:27:41 -07:00
parent 3e8623d383
commit 8e790a0e15
9 changed files with 251 additions and 159 deletions

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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>

View file

@ -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)}

View file

@ -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>
);
}

View 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;

View file

@ -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>
)
}
}

View file

@ -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>
)
}

View file

@ -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}&#37; failed
</span>
<span
style={{color: 'green'}}
title={`${this.state.counts.succeeded} out of ${total}`}
>
{this.state.counts.succeeded / total * 100}&#37; 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}&#37; 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:&nbsp;
<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}&#37; failed
</span>
<span
style={{color: 'green'}}
title={`${this.state.counts.succeeded} out of ${total}`}
>
{this.state.counts.succeeded / total * 100}&#37; 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}&#37; 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:&nbsp;
<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>
)
}
}