[dashboard] Put dashboard into work window

This commit is contained in:
Ben Gotow 2016-11-28 18:02:39 -08:00
parent 10f62f6b5a
commit f804c53522
28 changed files with 426 additions and 20610 deletions

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 333 B

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

@ -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',
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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,
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]) {

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