mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-04 03:34:28 +08:00
[dashboard] Put dashboard into work window
This commit is contained in:
parent
10f62f6b5a
commit
f804c53522
28 changed files with 426 additions and 20610 deletions
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 333 B |
|
@ -1,9 +1,14 @@
|
|||
/* eslint global-require: 0 */
|
||||
import {ComponentRegistry} from 'nylas-exports'
|
||||
import {createLogger} from './src/shared/logger'
|
||||
|
||||
export function activate() {
|
||||
global.Logger = createLogger('local-sync')
|
||||
require('./src/local-api/app.js');
|
||||
require('./src/local-sync-worker/app.js');
|
||||
require('./src/local-api/app');
|
||||
require('./src/local-sync-worker/app');
|
||||
|
||||
const Root = require('./src/local-sync-dashboard/root').default;
|
||||
ComponentRegistry.register(Root, {role: 'Developer:LocalSyncUI'});
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Inert = require('inert');
|
||||
const Hapi = require('hapi');
|
||||
const HapiWebSocket = require('hapi-plugin-websocket');
|
||||
|
||||
const server = new Hapi.Server();
|
||||
server.connection({ port: process.env.PORT });
|
||||
|
||||
const attach = (directory) => {
|
||||
const routesDir = path.join(__dirname, directory)
|
||||
fs.readdirSync(routesDir).forEach((filename) => {
|
||||
if (filename.endsWith('.js')) {
|
||||
const routeFactory = require(path.join(routesDir, filename));
|
||||
routeFactory(server);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
server.register([HapiWebSocket, Inert], () => {
|
||||
attach('./routes/')
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/ping',
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
global.Logger.info("---> Ping!")
|
||||
reply("pong")
|
||||
},
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/{param*}',
|
||||
handler: {
|
||||
directory: {
|
||||
path: require('path').join(__dirname, 'public'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
server.start((startErr) => {
|
||||
if (startErr) { throw startErr; }
|
||||
global.Logger.info({uri: server.info.uri}, 'Dashboard running');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
const React = window.React;
|
||||
import {React} from 'nylas-exports';
|
||||
|
||||
class Dropdown extends React.Component {
|
||||
export default class Dropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -67,5 +67,3 @@ Dropdown.propTypes = {
|
|||
defaultOption: React.PropTypes.string,
|
||||
onSelect: React.PropTypes.func,
|
||||
}
|
||||
|
||||
window.Dropdown = Dropdown;
|
|
@ -1,12 +1,11 @@
|
|||
const React = window.React;
|
||||
const ReactDOM = window.ReactDOM;
|
||||
import {React, ReactDOM} from 'nylas-exports';
|
||||
|
||||
setInterval(() => {
|
||||
const event = new Event('tick');
|
||||
window.dispatchEvent(event);
|
||||
}, 1000);
|
||||
|
||||
class ElapsedTime extends React.Component {
|
||||
export default class ElapsedTime extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -36,5 +35,3 @@ ElapsedTime.propTypes = {
|
|||
refTimestamp: React.PropTypes.number, // milliseconds
|
||||
formatTime: React.PropTypes.func,
|
||||
}
|
||||
|
||||
window.ElapsedTime = ElapsedTime;
|
|
@ -1,6 +1,6 @@
|
|||
const React = window.React;
|
||||
import {React} from 'nylas-exports';
|
||||
|
||||
class Modal extends React.Component {
|
||||
export default class Modal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -101,5 +101,3 @@ Modal.propTypes = {
|
|||
onClose: React.PropTypes.func,
|
||||
actionElems: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
}
|
||||
|
||||
window.Modal = Modal;
|
|
@ -1,21 +0,0 @@
|
|||
const {NODE_ENV} = process.env
|
||||
/**
|
||||
* New Relic agent configuration.
|
||||
*
|
||||
* See lib/config.defaults.js in the agent distribution for a more complete
|
||||
* description of configuration variables and their potential values.
|
||||
*/
|
||||
exports.config = {
|
||||
/**
|
||||
* Array of application names.
|
||||
*/
|
||||
app_name: [`k2-dash-${NODE_ENV}`],
|
||||
logging: {
|
||||
/**
|
||||
* Level at which to log. 'trace' is most useful to New Relic when diagnosing
|
||||
* issues with the agent, 'info' and higher will impose the least overhead on
|
||||
* production applications.
|
||||
*/
|
||||
level: 'info',
|
||||
},
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
body {
|
||||
background-image: url(http://news.nationalgeographic.com/content/dam/news/2015/12/13/BookTalk%20K2/01BookTalkK2.jpg);
|
||||
background-image: -moz-linear-gradient(top, rgba(232, 244, 250, 0.2), rgba(231, 231, 233, 1)), url(http://news.nationalgeographic.com/content/dam/news/2015/12/13/BookTalk%20K2/01BookTalkK2.jpg);
|
||||
background-image: -webkit-linear-gradient(top, rgba(232, 244, 250, 0.2), rgba(231, 231, 233, 1)), url(http://news.nationalgeographic.com/content/dam/news/2015/12/13/BookTalk%20K2/01BookTalkK2.jpg);
|
||||
background-size: 100vw auto;
|
||||
background-attachment: fixed;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#accounts-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.account {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
width: 240px;
|
||||
height: 450px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 15px;
|
||||
margin: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.account h3 {
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.account .section {
|
||||
font-size: 12px;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account.errored {
|
||||
color: #a94442;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(231, 195, 195);
|
||||
}
|
||||
|
||||
.error-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-link:hover {
|
||||
cursor: pointer;
|
||||
color: #702726;
|
||||
}
|
||||
|
||||
#open-all-sync {
|
||||
color: #ffffff;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.right-action {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: rgba(16, 83, 161, 0.88);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.action-link.cancel {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sync-policy textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: white;
|
||||
width: 50%;
|
||||
margin: 10vh auto;
|
||||
padding: 20px;
|
||||
max-height: calc(80vh - 40px); /* minus padding */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-bg {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.modal-close-wrapper {
|
||||
position: relative;
|
||||
height: 0;
|
||||
width: 0;
|
||||
float: right;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
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 {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.stats b {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#syncback-request-details {
|
||||
font-size: 15px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#syncback-request-details .counts {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#syncback-request-details span {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#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) {
|
||||
background-color: #F1F1F1;
|
||||
}
|
||||
|
||||
#syncback-request-details tr:not(:first-child):hover {
|
||||
background-color: #C9C9C9;
|
||||
}
|
||||
|
||||
#syncback-request-details td, #syncback-request-details th {
|
||||
text-align: center;
|
||||
padding: 10px 5px;
|
||||
border: solid black 1px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 0 5px;
|
||||
height: 7px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown-options {
|
||||
border: solid black 1px;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
position: relative;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
.dropdown-option:hover {
|
||||
background-color: rgb(114, 163, 255);
|
||||
}
|
||||
|
||||
.dropdown-selected {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dropdown-wrapper {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.mini-account::after {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #666666;
|
||||
content: "";
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.mini-account {
|
||||
background-color: rgb(0, 255, 157);
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.mini-account.errored {
|
||||
background-color: rgb(255, 38, 0);
|
||||
}
|
||||
|
||||
.process-loads {
|
||||
display: inline-block;
|
||||
padding: 15px;
|
||||
width: 250px;
|
||||
margin: 15px 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.process-loads .section {
|
||||
text-decoration: underline;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sum-accounts {
|
||||
border-top: solid black 1px;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.account-filter {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.process-group {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
max-width: 250px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#group-by-process {
|
||||
vertical-align: middle;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.4 KiB |
|
@ -1,27 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<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="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script>
|
||||
<script src="/js/elapsed-time.jsx" type="text/babel"></script>
|
||||
<script src="/js/process-loads.jsx" type="text/babel"></script>
|
||||
<script src="/js/mini-account.jsx" type="text/babel"></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>
|
||||
<script src="/js/sync-graph.jsx" type="text/babel"></script>
|
||||
<script src="/js/dropdown.jsx" type="text/babel"></script>
|
||||
<script src="/js/syncback-request-details.jsx" type="text/babel"></script>
|
||||
<script src="/js/app.jsx" type="text/babel"></script>
|
||||
<link rel='stylesheet' type="text/css" href="./css/app.css" />
|
||||
<link rel='shortcut icon' href='favicon.png' / >
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'>
|
||||
<title>K2 Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>K2 Dashboard</h2>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
const React = window.React;
|
||||
|
||||
function AccountFilter(props) {
|
||||
return (
|
||||
<div className="account-filter">
|
||||
Display: <select {...props}>
|
||||
<option value={AccountFilter.states.all}>All Accounts</option>
|
||||
<option value={AccountFilter.states.errored}>Accounts with Errors</option>
|
||||
<option value={AccountFilter.states.notErrored}>Accounts without Errors</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
AccountFilter.propTypes = {
|
||||
onChange: React.PropTypes.func,
|
||||
id: React.PropTypes.string,
|
||||
}
|
||||
|
||||
AccountFilter.states = {
|
||||
all: "all",
|
||||
errored: "errored",
|
||||
notErrored: "not-errored",
|
||||
};
|
||||
|
||||
window.AccountFilter = AccountFilter;
|
|
@ -1,333 +0,0 @@
|
|||
/* eslint react/react-in-jsx-scope: 0*/
|
||||
/* eslint no-console: 0*/
|
||||
|
||||
const React = window.React;
|
||||
const ReactDOM = window.ReactDOM;
|
||||
const _ = window._;
|
||||
const {
|
||||
SyncPolicy,
|
||||
SetAllSyncPolicies,
|
||||
AccountFilter,
|
||||
SyncGraph,
|
||||
SyncbackRequestDetails,
|
||||
ElapsedTime,
|
||||
Modal,
|
||||
MiniAccount,
|
||||
ProcessLoads,
|
||||
} = window;
|
||||
|
||||
function calcAcctPosition(count) {
|
||||
const width = 280;
|
||||
const height = 490;
|
||||
const marginTop = 0;
|
||||
const marginSide = 0;
|
||||
|
||||
const acctsPerRow = Math.floor((window.innerWidth - 2 * marginSide) / width);
|
||||
const row = Math.floor(count / acctsPerRow)
|
||||
const col = count - (row * acctsPerRow);
|
||||
const top = marginTop + (row * height);
|
||||
const left = marginSide + (width * col);
|
||||
|
||||
return {left: left, top: top};
|
||||
}
|
||||
|
||||
function formatSyncTimes(timestamp) {
|
||||
return timestamp / 1000;
|
||||
}
|
||||
|
||||
class Account extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
accountId: props.account.id,
|
||||
version: null,
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.account.version !== this.props.account.version ||
|
||||
nextProps.active !== this.props.active ||
|
||||
nextProps.assignment !== this.props.assignment ||
|
||||
nextProps.count !== this.props.count;
|
||||
}
|
||||
|
||||
clearError() {
|
||||
const req = new XMLHttpRequest();
|
||||
const url = `${window.location.protocol}/accounts/${this.state.accountId}/clear-sync-error`;
|
||||
req.open("PUT", url, true);
|
||||
req.onreadystatechange = () => {
|
||||
if (req.readyState === XMLHttpRequest.DONE) {
|
||||
if (req.status === 200) {
|
||||
// Would setState here, but external updates currently refresh the account
|
||||
} else {
|
||||
console.error(req.responseText);
|
||||
}
|
||||
}
|
||||
}
|
||||
req.send();
|
||||
}
|
||||
|
||||
renderPolicyOrError() {
|
||||
const account = this.props.account;
|
||||
if (account.sync_error != null) {
|
||||
return this.renderError();
|
||||
}
|
||||
return (
|
||||
<SyncPolicy
|
||||
accountId={account.id}
|
||||
stringifiedSyncPolicy={JSON.stringify(account.sync_policy, null, 2)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderError() {
|
||||
const {message, stack} = this.props.account.sync_error
|
||||
return (
|
||||
<div>
|
||||
<div className="section">Error</div>
|
||||
<Modal
|
||||
openLink={{text: message, className: 'error-link'}}
|
||||
>
|
||||
<pre>{JSON.stringify(stack, null, 2)}</pre>
|
||||
</Modal>
|
||||
<div className="action-link" onClick={() => this.clearError()}>Clear Error</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {account, assignment, active} = this.props;
|
||||
const errorClass = account.sync_error ? ' errored' : ''
|
||||
|
||||
const numStoredSyncs = account.last_sync_completions.length;
|
||||
const oldestSync = account.last_sync_completions[numStoredSyncs - 1];
|
||||
const newestSync = account.last_sync_completions[0];
|
||||
const avgBetweenSyncs = (newestSync - oldestSync) / (1000 * numStoredSyncs);
|
||||
|
||||
let firstSyncDuration = "Incomplete";
|
||||
if (account.first_sync_completion) {
|
||||
firstSyncDuration = (new Date(account.first_sync_completion) - new Date(account.created_at)) / 1000;
|
||||
}
|
||||
|
||||
const position = calcAcctPosition(this.props.count);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`account${errorClass}`}
|
||||
style={{top: `${position.top}px`, left: `${position.left}px`}}
|
||||
>
|
||||
<h3>{account.email_address} [{account.id}] {active ? '🌕' : '🌑'}</h3>
|
||||
<strong>{assignment}</strong>
|
||||
<SyncbackRequestDetails accountId={account.id} />
|
||||
<div className="stats">
|
||||
<b>First Sync Duration (sec)</b>:
|
||||
<pre>{firstSyncDuration}</pre>
|
||||
<b> Average Time Between Syncs (sec)</b>:
|
||||
<pre>{avgBetweenSyncs}</pre>
|
||||
<b>Time Since Last Sync (sec)</b>:
|
||||
<pre>
|
||||
<ElapsedTime refTimestamp={newestSync} formatTime={formatSyncTimes} />
|
||||
</pre>
|
||||
<b>Recent Syncs</b>:
|
||||
<SyncGraph id={account.last_sync_completions.length} syncTimestamps={account.last_sync_completions} />
|
||||
</div>
|
||||
{this.renderPolicyOrError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Account.propTypes = {
|
||||
account: React.PropTypes.object,
|
||||
active: React.PropTypes.bool,
|
||||
assignment: React.PropTypes.string,
|
||||
count: React.PropTypes.number,
|
||||
}
|
||||
|
||||
class Root extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
accounts: {},
|
||||
assignments: {},
|
||||
activeAccountIds: [],
|
||||
visibleAccounts: AccountFilter.states.all,
|
||||
groupByProcess: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let url = null;
|
||||
if (window.location.protocol === "https:") {
|
||||
url = `wss://${window.location.host}/websocket`;
|
||||
} else {
|
||||
url = `ws://${window.location.host}/websocket`;
|
||||
}
|
||||
this.websocket = new WebSocket(url);
|
||||
this.websocket.onopen = () => {
|
||||
this.websocket.send("Message to send");
|
||||
};
|
||||
this.websocket.onmessage = (evt) => {
|
||||
try {
|
||||
const msg = JSON.parse(evt.data);
|
||||
if (msg.cmd === 'UPDATE') {
|
||||
this.onReceivedUpdate(msg.payload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
this.websocket.onclose = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
}
|
||||
|
||||
onReceivedUpdate(update) {
|
||||
const accounts = Object.assign({}, this.state.accounts);
|
||||
for (const account of update.updatedAccounts) {
|
||||
if (accounts[account.id]) {
|
||||
account.version = accounts[account.id].version + 1;
|
||||
} else {
|
||||
account.version = 0;
|
||||
}
|
||||
accounts[account.id] = account;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
assignments: update.assignments || this.state.assignments,
|
||||
activeAccountIds: update.activeAccountIds || this.state.activeAccountIds,
|
||||
accounts: accounts,
|
||||
processLoads: update.processLoads,
|
||||
})
|
||||
}
|
||||
|
||||
onFilter() {
|
||||
this.setState({visibleAccounts: document.getElementById('account-filter').value});
|
||||
}
|
||||
|
||||
onGroupChange() {
|
||||
this.setState({
|
||||
groupByProcess: document.getElementById('group-by-process').checked,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let ids = Object.keys(this.state.accounts);
|
||||
|
||||
switch (this.state.visibleAccounts) {
|
||||
case AccountFilter.states.errored:
|
||||
ids = ids.filter((id) => this.state.accounts[id].sync_error)
|
||||
break;
|
||||
case AccountFilter.states.notErrored:
|
||||
ids = ids.filter((id) => !this.state.accounts[id].sync_error)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.props.collapsed) {
|
||||
const groupByProcess = (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="group-by-process"
|
||||
onChange={() => this.onGroupChange()}
|
||||
/>
|
||||
Group Accounts By Process
|
||||
</div>
|
||||
)
|
||||
|
||||
if (this.state.groupByProcess) {
|
||||
const accountsById = _.groupBy(this.state.accounts, 'id');
|
||||
const processes = [];
|
||||
|
||||
for (const processName of Object.keys(this.state.processLoads)) {
|
||||
const accounts = []
|
||||
|
||||
for (const accountId of this.state.processLoads[processName]) {
|
||||
const account = accountsById[accountId][0];
|
||||
accounts.push((
|
||||
<MiniAccount key={accountId} account={account} />
|
||||
))
|
||||
}
|
||||
processes.push((
|
||||
<div key={processName} title={`Process: ${processName}`} className="process-group">
|
||||
{accounts}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
content = (
|
||||
<div>
|
||||
{groupByProcess}
|
||||
<div id="accounts-wrapper">
|
||||
{processes}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
{groupByProcess}
|
||||
<div id="accounts-wrapper">
|
||||
{
|
||||
ids.sort((a, b) => a / 1 - b / 1).map((id) =>
|
||||
<MiniAccount
|
||||
key={id}
|
||||
account={this.state.accounts[id]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let count = 0;
|
||||
content = (
|
||||
<div id="accounts-wrapper">
|
||||
{
|
||||
ids.sort((a, b) => a / 1 - b / 1).map((id) =>
|
||||
<Account
|
||||
key={id}
|
||||
active={this.state.activeAccountIds.includes(id)}
|
||||
assignment={this.state.assignments[id]}
|
||||
account={this.state.accounts[id]}
|
||||
count={count++}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProcessLoads loads={this.state.processLoads} />
|
||||
<AccountFilter id="account-filter" onChange={() => this.onFilter.call(this)} />
|
||||
<SetAllSyncPolicies accountIds={ids.map((id) => parseInt(id, 10))} />
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Root.propTypes = {
|
||||
collapsed: React.PropTypes.bool,
|
||||
}
|
||||
|
||||
let collapsed = false;
|
||||
const collapsedStr = "collapsed";
|
||||
const index = window.location.search.indexOf(collapsedStr);
|
||||
if (index >= 0) {
|
||||
const value = window.location.search.substring(index + collapsedStr.length + 1);
|
||||
if (value.startsWith("true")) {
|
||||
collapsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Root collapsed={collapsed} />,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -1,40 +0,0 @@
|
|||
const React = window.React;
|
||||
|
||||
class MiniAccount extends React.Component {
|
||||
|
||||
calculateColor() {
|
||||
// in milliseconds
|
||||
const grayAfter = 1000 * 60 * 10; // 10 minutes
|
||||
const elapsedTime = Date.now() - this.props.account.last_sync_completions[0];
|
||||
let opacity = 0;
|
||||
if (elapsedTime < grayAfter) {
|
||||
opacity = 1.0 - elapsedTime / grayAfter;
|
||||
}
|
||||
|
||||
return `rgba(0, 255, 157, ${opacity})`;
|
||||
}
|
||||
|
||||
render() {
|
||||
let errorClass;
|
||||
const style = {};
|
||||
if (this.props.account.sync_error) {
|
||||
errorClass = 'errored';
|
||||
} else {
|
||||
errorClass = '';
|
||||
style.backgroundColor = this.calculateColor();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mini-account ${errorClass}`}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MiniAccount.propTypes = {
|
||||
account: React.PropTypes.object,
|
||||
};
|
||||
|
||||
window.MiniAccount = MiniAccount;
|
|
@ -1,37 +0,0 @@
|
|||
const React = window.React;
|
||||
|
||||
function ProcessLoads(props) {
|
||||
let entries;
|
||||
let sumElem;
|
||||
if (props.loads == null || Object.keys(props.loads).length === 0) {
|
||||
entries = "No Data";
|
||||
sumElem = "";
|
||||
} else {
|
||||
entries = [];
|
||||
let sum = 0;
|
||||
for (const processName of Object.keys(props.loads).sort()) {
|
||||
const count = props.loads[processName].length;
|
||||
sum += count;
|
||||
entries.push(
|
||||
<div className="load-count" key={processName}>
|
||||
<b>{processName}</b>: {count} accounts
|
||||
</div>
|
||||
);
|
||||
}
|
||||
sumElem = <div className="sum-accounts">Total Accounts: {sum} </div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="process-loads">
|
||||
<div className="section">Process Loads </div>
|
||||
{entries}
|
||||
{sumElem}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ProcessLoads.propTypes = {
|
||||
loads: React.PropTypes.object,
|
||||
}
|
||||
|
||||
window.ProcessLoads = ProcessLoads;
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* ReactDOM v15.1.0
|
||||
*
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
|
||||
;(function(f) {
|
||||
// CommonJS
|
||||
if (typeof exports === "object" && typeof module !== "undefined") {
|
||||
module.exports = f(require('react'));
|
||||
|
||||
// RequireJS
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define(['react'], f);
|
||||
|
||||
// <script>
|
||||
} else {
|
||||
var g;
|
||||
if (typeof window !== "undefined") {
|
||||
g = window;
|
||||
} else if (typeof global !== "undefined") {
|
||||
g = global;
|
||||
} else if (typeof self !== "undefined") {
|
||||
g = self;
|
||||
} else {
|
||||
// works providing we're not in "use strict";
|
||||
// needed for Java 8 Nashorn
|
||||
// see https://github.com/facebook/react/issues/3037
|
||||
g = this;
|
||||
}
|
||||
g.ReactDOM = f(g.React);
|
||||
}
|
||||
|
||||
})(function(React) {
|
||||
return React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* ReactDOM v15.1.0
|
||||
*
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOM=e(f.React)}}(function(e){return e.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED});
|
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
181
packages/local-sync/src/local-sync-dashboard/root.jsx
Normal file
181
packages/local-sync/src/local-sync-dashboard/root.jsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
/* eslint react/react-in-jsx-scope: 0*/
|
||||
/* eslint no-console: 0*/
|
||||
/* eslint global-require: 0*/
|
||||
|
||||
import {React} from 'nylas-exports';
|
||||
import SyncPolicy from './sync-policy';
|
||||
import SetAllSyncPolicies from './set-all-sync-policies';
|
||||
import SyncGraph from './sync-graph';
|
||||
import SyncbackRequestDetails from './syncback-request-details';
|
||||
import ElapsedTime from './elapsed-time';
|
||||
import Modal from './modal';
|
||||
|
||||
import LocalDatabaseConnector from '../shared/local-database-connector';
|
||||
import SyncProcessManager from '../local-sync-worker/sync-process-manager';
|
||||
|
||||
function calcAcctPosition(count) {
|
||||
const width = 280;
|
||||
const height = 490;
|
||||
const marginTop = 0;
|
||||
const marginSide = 0;
|
||||
|
||||
const acctsPerRow = Math.floor((window.innerWidth - 2 * marginSide) / width);
|
||||
const row = Math.floor(count / acctsPerRow)
|
||||
const col = count - (row * acctsPerRow);
|
||||
const top = marginTop + (row * height);
|
||||
const left = marginSide + (width * col);
|
||||
|
||||
return {left: left, top: top};
|
||||
}
|
||||
|
||||
function formatSyncTimes(timestamp) {
|
||||
return timestamp / 1000;
|
||||
}
|
||||
|
||||
class AccountCard extends React.Component {
|
||||
static propTypes = {
|
||||
account: React.PropTypes.object,
|
||||
count: React.PropTypes.number,
|
||||
};
|
||||
|
||||
onClearError = () => {
|
||||
LocalDatabaseConnector.forShared().then(({Account}) => {
|
||||
Account.find({where: {id: this.props.account.id}}).then((account) => {
|
||||
account.syncError = null;
|
||||
account.save().then(() => {
|
||||
SyncProcessManager.wakeWorkerForAccount(account);
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
renderPolicyOrError() {
|
||||
const account = this.props.account;
|
||||
if (account.syncError != null) {
|
||||
return this.renderError();
|
||||
}
|
||||
return (
|
||||
<SyncPolicy
|
||||
accountId={account.id}
|
||||
stringifiedSyncPolicy={JSON.stringify(account.syncPolicy, null, 2)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderError() {
|
||||
const {message, stack} = this.props.account.syncError
|
||||
return (
|
||||
<div>
|
||||
<div className="section">Error</div>
|
||||
<Modal
|
||||
openLink={{text: message, className: 'error-link'}}
|
||||
>
|
||||
<pre>{JSON.stringify(stack, null, 2)}</pre>
|
||||
</Modal>
|
||||
<div className="action-link" onClick={this.onClearError}>Clear Error</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {account} = this.props;
|
||||
const errorClass = account.syncError ? ' errored' : ''
|
||||
|
||||
const numStoredSyncs = account.lastSyncCompletions.length;
|
||||
const oldestSync = account.lastSyncCompletions[numStoredSyncs - 1];
|
||||
const newestSync = account.lastSyncCompletions[0];
|
||||
const avgBetweenSyncs = (newestSync - oldestSync) / (1000 * numStoredSyncs);
|
||||
|
||||
let firstSyncDuration = "Incomplete";
|
||||
if (account.firstSyncCompletion) {
|
||||
firstSyncDuration = (new Date(account.firstSyncCompletion) - new Date(account.createdAt)) / 1000;
|
||||
}
|
||||
|
||||
const position = calcAcctPosition(this.props.count);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`account${errorClass}`}
|
||||
style={{top: `${position.top}px`, left: `${position.left}px`}}
|
||||
>
|
||||
<h3>{account.emailAddress} [{account.id}]</h3>
|
||||
<SyncbackRequestDetails accountId={account.id} />
|
||||
<div className="stats">
|
||||
<b>First Sync Duration (sec)</b>:
|
||||
<pre>{firstSyncDuration}</pre>
|
||||
<b> Average Time Between Syncs (sec)</b>:
|
||||
<pre>{avgBetweenSyncs}</pre>
|
||||
<b>Time Since Last Sync (sec)</b>:
|
||||
<pre>
|
||||
<ElapsedTime refTimestamp={newestSync} formatTime={formatSyncTimes} />
|
||||
</pre>
|
||||
<b>Recent Syncs</b>:
|
||||
<SyncGraph id={account.lastSyncCompletions.length} syncTimestamps={account.lastSyncCompletions} />
|
||||
</div>
|
||||
{this.renderPolicyOrError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class Root extends React.Component {
|
||||
static displayName = 'Root';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
accounts: {},
|
||||
assignments: {},
|
||||
activeAccountIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// just periodically poll. This is crazy nasty and violates separation of
|
||||
// concerns, but oh well. Replace it later.
|
||||
|
||||
this._timer = setInterval(() => {
|
||||
LocalDatabaseConnector.forShared().then(({Account}) => {
|
||||
Account.findAll().then((accounts) => {
|
||||
this.setState({accounts});
|
||||
});
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this._timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
const ids = Object.keys(this.state.accounts);
|
||||
let count = 0;
|
||||
const content = (
|
||||
<div id="accounts-wrapper">
|
||||
{
|
||||
ids.sort((a, b) => a / 1 - b / 1).map((id) =>
|
||||
<AccountCard
|
||||
key={id}
|
||||
active={this.state.activeAccountIds.includes(id)}
|
||||
assignment={this.state.assignments[id]}
|
||||
account={this.state.accounts[id]}
|
||||
count={count++}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SetAllSyncPolicies accountIds={ids.map((id) => parseInt(id, 10))} />
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Root.propTypes = {
|
||||
collapsed: React.PropTypes.bool,
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
const Joi = require('joi');
|
||||
const LocalDatabaseConnector = require('../../shared/local-database-connector');
|
||||
|
||||
module.exports = (server) => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: '/accounts/{accountId}/clear-sync-error',
|
||||
config: {
|
||||
description: 'Clears the sync error for the given account',
|
||||
notes: 'Notes go here',
|
||||
tags: ['accounts', 'sync-error'],
|
||||
validate: {
|
||||
params: {
|
||||
accountId: Joi.number().integer(),
|
||||
},
|
||||
},
|
||||
response: {
|
||||
schema: Joi.string(),
|
||||
},
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
LocalDatabaseConnector.forShared().then(({Account}) => {
|
||||
Account.find({where: {id: request.params.accountId}}).then((account) => {
|
||||
account.syncError = null;
|
||||
account.save().then(() => reply("Success"));
|
||||
})
|
||||
})
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,81 +0,0 @@
|
|||
const Joi = require('joi');
|
||||
const LocalDatabaseConnector = require('../../shared/local-database-connector');
|
||||
|
||||
module.exports = (server) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/syncback-requests/{account_id}',
|
||||
config: {
|
||||
description: 'Get the SyncbackRequests for an account',
|
||||
notes: 'Notes go here',
|
||||
tags: ['syncback-requests'],
|
||||
validate: {
|
||||
params: {
|
||||
account_id: Joi.number().integer(),
|
||||
},
|
||||
},
|
||||
response: {
|
||||
schema: Joi.string(),
|
||||
},
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
LocalDatabaseConnector.forAccount(request.params.account_id).then((db) => {
|
||||
const {SyncbackRequest} = db;
|
||||
SyncbackRequest.findAll().then((syncbackRequests) => {
|
||||
reply(JSON.stringify(syncbackRequests))
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/syncback-requests/{account_id}/counts',
|
||||
config: {
|
||||
description: 'Get stats on the statuses of SyncbackRequests',
|
||||
notes: 'Notes go here',
|
||||
tags: ['syncback-requests'],
|
||||
validate: {
|
||||
params: {
|
||||
account_id: Joi.number().integer(),
|
||||
},
|
||||
query: {
|
||||
since: Joi.date().timestamp(),
|
||||
},
|
||||
},
|
||||
response: {
|
||||
schema: Joi.string(),
|
||||
},
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
LocalDatabaseConnector.forAccount(request.params.account_id).then((db) => {
|
||||
const {SyncbackRequest} = db;
|
||||
|
||||
const counts = {
|
||||
'new': null,
|
||||
'succeeded': null,
|
||||
'failed': null,
|
||||
}
|
||||
|
||||
const where = {};
|
||||
if (request.query.since) {
|
||||
where.createdAt = {gt: request.query.since};
|
||||
}
|
||||
|
||||
const countPromises = [];
|
||||
for (const status of Object.keys(counts)) {
|
||||
where.status = status.toUpperCase();
|
||||
countPromises.push(
|
||||
SyncbackRequest.count({where: where}).then((count) => {
|
||||
counts[status] = count;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Promise.all(countPromises).then(() => {
|
||||
reply(JSON.stringify(counts));
|
||||
})
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
const React = window.React;
|
||||
const Modal = window.Modal;
|
||||
import {React} from 'nylas-exports';
|
||||
import Modal from './modal';
|
||||
|
||||
class SetAllSyncPolicies extends React.Component {
|
||||
export default class SetAllSyncPolicies extends React.Component {
|
||||
|
||||
applyToAllAccounts(accountIds) {
|
||||
const req = new XMLHttpRequest();
|
||||
|
@ -55,5 +55,3 @@ class SetAllSyncPolicies extends React.Component {
|
|||
SetAllSyncPolicies.propTypes = {
|
||||
accountIds: React.PropTypes.arrayOf(React.PropTypes.number),
|
||||
}
|
||||
|
||||
window.SetAllSyncPolicies = SetAllSyncPolicies;
|
|
@ -1,12 +1,11 @@
|
|||
const React = window.React;
|
||||
const ReactDOM = window.ReactDOM;
|
||||
import {React, ReactDOM} from 'nylas-exports';
|
||||
|
||||
setInterval(() => {
|
||||
const event = new Event('graphtick')
|
||||
window.dispatchEvent(event);
|
||||
}, 10000);
|
||||
|
||||
class SyncGraph extends React.Component {
|
||||
export default class SyncGraph extends React.Component {
|
||||
componentDidMount() {
|
||||
this.drawGraph(true);
|
||||
|
||||
|
@ -86,7 +85,6 @@ class SyncGraph extends React.Component {
|
|||
width={SyncGraph.config.width}
|
||||
height={SyncGraph.config.height + SyncGraph.config.labelFontSize + SyncGraph.config.labelTopMargin}
|
||||
className="sync-graph"
|
||||
syncTimestamps={this.props.syncTimestamps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -112,5 +110,3 @@ SyncGraph.config = {
|
|||
SyncGraph.propTypes = {
|
||||
syncTimestamps: React.PropTypes.arrayOf(React.PropTypes.number),
|
||||
}
|
||||
|
||||
window.SyncGraph = SyncGraph;
|
|
@ -1,6 +1,6 @@
|
|||
const React = window.React;
|
||||
import {React} from 'nylas-exports';
|
||||
|
||||
class SyncPolicy extends React.Component {
|
||||
export default class SyncPolicy extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {editMode: false};
|
||||
|
@ -61,5 +61,3 @@ SyncPolicy.propTypes = {
|
|||
accountId: React.PropTypes.number,
|
||||
stringifiedSyncPolicy: React.PropTypes.string,
|
||||
}
|
||||
|
||||
window.SyncPolicy = SyncPolicy;
|
|
@ -1,7 +1,8 @@
|
|||
const React = window.React;
|
||||
const {Dropdown, Modal} = window;
|
||||
import {React} from 'nylas-exports';
|
||||
import Dropdown from './dropdown';
|
||||
import Modal from './modal';
|
||||
|
||||
class SyncbackRequestDetails extends React.Component {
|
||||
export default class SyncbackRequestDetails extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -170,5 +171,3 @@ class SyncbackRequestDetails extends React.Component {
|
|||
SyncbackRequestDetails.propTypes = {
|
||||
accountId: React.PropTypes.number,
|
||||
}
|
||||
|
||||
window.SyncbackRequestDetails = SyncbackRequestDetails;
|
|
@ -31,7 +31,7 @@ class SyncProcessManager {
|
|||
this._workers = {};
|
||||
this._listenForSyncsClient = null;
|
||||
this._exiting = false;
|
||||
this._logger = global.Logger.child({identity: IDENTITY})
|
||||
this._logger = global.Logger.child({identity: IDENTITY});
|
||||
}
|
||||
|
||||
start() {
|
||||
|
@ -45,6 +45,10 @@ class SyncProcessManager {
|
|||
}));
|
||||
}
|
||||
|
||||
wakeWorkerForAccount(account) {
|
||||
this._workers[account.id].syncNow();
|
||||
}
|
||||
|
||||
addWorkerForAccount(account) {
|
||||
return LocalDatabaseConnector.forAccount(account.id).then((db) => {
|
||||
if (this._workers[account.id]) {
|
||||
|
|
216
packages/local-sync/stylesheets/index.less
Normal file
216
packages/local-sync/stylesheets/index.less
Normal file
|
@ -0,0 +1,216 @@
|
|||
.developer-bar .local-sync {
|
||||
#accounts-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.account {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
width: 270px;
|
||||
height: 450px;
|
||||
color: black;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 15px;
|
||||
margin: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.account h3 {
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.account .section {
|
||||
font-size: 12px;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account.errored {
|
||||
color: #a94442;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(231, 195, 195);
|
||||
}
|
||||
|
||||
.error-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-link:hover {
|
||||
cursor: pointer;
|
||||
color: #702726;
|
||||
}
|
||||
|
||||
#open-all-sync {
|
||||
color: #ffffff;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.right-action {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: rgba(16, 83, 161, 0.88);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.action-link.cancel {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sync-policy textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: white;
|
||||
width: 50%;
|
||||
margin: 10vh auto;
|
||||
padding: 20px;
|
||||
max-height: calc(80vh - 40px); /* minus padding */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-bg {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.modal-close-wrapper {
|
||||
position: relative;
|
||||
height: 0;
|
||||
width: 0;
|
||||
float: right;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
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 {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.stats b {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#syncback-request-details {
|
||||
font-size: 15px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#syncback-request-details .counts {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#syncback-request-details span {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#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) {
|
||||
background-color: #F1F1F1;
|
||||
}
|
||||
|
||||
#syncback-request-details tr:not(:first-child):hover {
|
||||
background-color: #C9C9C9;
|
||||
}
|
||||
|
||||
#syncback-request-details td, #syncback-request-details th {
|
||||
text-align: center;
|
||||
padding: 10px 5px;
|
||||
border: solid black 1px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 0 5px;
|
||||
height: 7px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown-options {
|
||||
border: solid black 1px;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
position: relative;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
.dropdown-option:hover {
|
||||
background-color: rgb(114, 163, 255);
|
||||
}
|
||||
|
||||
.dropdown-selected {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dropdown-wrapper {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.mini-account::after {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #666666;
|
||||
content: "";
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.mini-account {
|
||||
background-color: rgb(0, 255, 157);
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.mini-account.errored {
|
||||
background-color: rgb(255, 38, 0);
|
||||
}
|
||||
|
||||
.sum-accounts {
|
||||
border-top: solid black 1px;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue