mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-07 05:04:58 +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'
|
import {createLogger} from './src/shared/logger'
|
||||||
|
|
||||||
export function activate() {
|
export function activate() {
|
||||||
global.Logger = createLogger('local-sync')
|
global.Logger = createLogger('local-sync')
|
||||||
require('./src/local-api/app.js');
|
require('./src/local-api/app');
|
||||||
require('./src/local-sync-worker/app.js');
|
require('./src/local-sync-worker/app');
|
||||||
|
|
||||||
|
const Root = require('./src/local-sync-dashboard/root').default;
|
||||||
|
ComponentRegistry.register(Root, {role: 'Developer:LocalSyncUI'});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -67,5 +67,3 @@ Dropdown.propTypes = {
|
||||||
defaultOption: React.PropTypes.string,
|
defaultOption: React.PropTypes.string,
|
||||||
onSelect: React.PropTypes.func,
|
onSelect: React.PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Dropdown = Dropdown;
|
|
|
@ -1,12 +1,11 @@
|
||||||
const React = window.React;
|
import {React, ReactDOM} from 'nylas-exports';
|
||||||
const ReactDOM = window.ReactDOM;
|
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const event = new Event('tick');
|
const event = new Event('tick');
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
class ElapsedTime extends React.Component {
|
export default class ElapsedTime extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -36,5 +35,3 @@ ElapsedTime.propTypes = {
|
||||||
refTimestamp: React.PropTypes.number, // milliseconds
|
refTimestamp: React.PropTypes.number, // milliseconds
|
||||||
formatTime: React.PropTypes.func,
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -101,5 +101,3 @@ Modal.propTypes = {
|
||||||
onClose: React.PropTypes.func,
|
onClose: React.PropTypes.func,
|
||||||
actionElems: React.PropTypes.arrayOf(React.PropTypes.object),
|
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;
|
import {React} from 'nylas-exports';
|
||||||
const Modal = window.Modal;
|
import Modal from './modal';
|
||||||
|
|
||||||
class SetAllSyncPolicies extends React.Component {
|
export default class SetAllSyncPolicies extends React.Component {
|
||||||
|
|
||||||
applyToAllAccounts(accountIds) {
|
applyToAllAccounts(accountIds) {
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
|
@ -55,5 +55,3 @@ class SetAllSyncPolicies extends React.Component {
|
||||||
SetAllSyncPolicies.propTypes = {
|
SetAllSyncPolicies.propTypes = {
|
||||||
accountIds: React.PropTypes.arrayOf(React.PropTypes.number),
|
accountIds: React.PropTypes.arrayOf(React.PropTypes.number),
|
||||||
}
|
}
|
||||||
|
|
||||||
window.SetAllSyncPolicies = SetAllSyncPolicies;
|
|
|
@ -1,12 +1,11 @@
|
||||||
const React = window.React;
|
import {React, ReactDOM} from 'nylas-exports';
|
||||||
const ReactDOM = window.ReactDOM;
|
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const event = new Event('graphtick')
|
const event = new Event('graphtick')
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
class SyncGraph extends React.Component {
|
export default class SyncGraph extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.drawGraph(true);
|
this.drawGraph(true);
|
||||||
|
|
||||||
|
@ -86,7 +85,6 @@ class SyncGraph extends React.Component {
|
||||||
width={SyncGraph.config.width}
|
width={SyncGraph.config.width}
|
||||||
height={SyncGraph.config.height + SyncGraph.config.labelFontSize + SyncGraph.config.labelTopMargin}
|
height={SyncGraph.config.height + SyncGraph.config.labelFontSize + SyncGraph.config.labelTopMargin}
|
||||||
className="sync-graph"
|
className="sync-graph"
|
||||||
syncTimestamps={this.props.syncTimestamps}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -112,5 +110,3 @@ SyncGraph.config = {
|
||||||
SyncGraph.propTypes = {
|
SyncGraph.propTypes = {
|
||||||
syncTimestamps: React.PropTypes.arrayOf(React.PropTypes.number),
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {editMode: false};
|
this.state = {editMode: false};
|
||||||
|
@ -61,5 +61,3 @@ SyncPolicy.propTypes = {
|
||||||
accountId: React.PropTypes.number,
|
accountId: React.PropTypes.number,
|
||||||
stringifiedSyncPolicy: React.PropTypes.string,
|
stringifiedSyncPolicy: React.PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
window.SyncPolicy = SyncPolicy;
|
|
|
@ -1,7 +1,8 @@
|
||||||
const React = window.React;
|
import {React} from 'nylas-exports';
|
||||||
const {Dropdown, Modal} = window;
|
import Dropdown from './dropdown';
|
||||||
|
import Modal from './modal';
|
||||||
|
|
||||||
class SyncbackRequestDetails extends React.Component {
|
export default class SyncbackRequestDetails extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -170,5 +171,3 @@ class SyncbackRequestDetails extends React.Component {
|
||||||
SyncbackRequestDetails.propTypes = {
|
SyncbackRequestDetails.propTypes = {
|
||||||
accountId: React.PropTypes.number,
|
accountId: React.PropTypes.number,
|
||||||
}
|
}
|
||||||
|
|
||||||
window.SyncbackRequestDetails = SyncbackRequestDetails;
|
|
|
@ -31,7 +31,7 @@ class SyncProcessManager {
|
||||||
this._workers = {};
|
this._workers = {};
|
||||||
this._listenForSyncsClient = null;
|
this._listenForSyncsClient = null;
|
||||||
this._exiting = false;
|
this._exiting = false;
|
||||||
this._logger = global.Logger.child({identity: IDENTITY})
|
this._logger = global.Logger.child({identity: IDENTITY});
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
@ -45,6 +45,10 @@ class SyncProcessManager {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wakeWorkerForAccount(account) {
|
||||||
|
this._workers[account.id].syncNow();
|
||||||
|
}
|
||||||
|
|
||||||
addWorkerForAccount(account) {
|
addWorkerForAccount(account) {
|
||||||
return LocalDatabaseConnector.forAccount(account.id).then((db) => {
|
return LocalDatabaseConnector.forAccount(account.id).then((db) => {
|
||||||
if (this._workers[account.id]) {
|
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