Dashboard with a sweet background. Also realtime assignment / policy view.

This commit is contained in:
Ben Gotow 2016-06-23 15:52:45 -07:00
parent 3f5cac4342
commit 09bb7874f8
18 changed files with 19938 additions and 21 deletions

View file

@ -7,6 +7,8 @@ const Package = require('./package');
const fs = require('fs');
const path = require('path');
global.Promise = require('bluebird');
const server = new Hapi.Server();
server.connection({ port: process.env.PORT || 5100 });

View file

@ -1,6 +1,5 @@
const Imap = require('imap');
const EventEmitter = require('events');
const Promise = require('bluebird');
const Capabilities = {
Gmail: 'X-GM-EXT-1',

View file

@ -1,7 +1,10 @@
global.Promise = require('bluebird');
module.exports = {
DatabaseConnector: require('./database-connector'),
PubsubConnector: require('./pubsub-connector'),
IMAPConnection: require('./imap-connection'),
SyncPolicy: require('./sync-policy'),
SchedulerUtils: require('./scheduler-utils'),
Config: require(`./config/${process.env.ENV || 'development'}`),
}

View file

@ -1,11 +1,10 @@
const Rx = require('rx')
const bluebird = require('bluebird')
const redis = require("redis");
const SyncPolicy = require('./sync-policy');
bluebird.promisifyAll(redis.RedisClient.prototype);
bluebird.promisifyAll(redis.Multi.prototype);
Promise.promisifyAll(redis.RedisClient.prototype);
Promise.promisifyAll(redis.Multi.prototype);
class PubsubConnector {
constructor() {
@ -32,7 +31,7 @@ class PubsubConnector {
}
channelForAccountDeltas(accountId) {
return `a-${accountId}-deltas`;
return `deltas-${accountId}`;
}
// Shared channel

View file

@ -0,0 +1,29 @@
const ACCOUNTS_UNCLAIMED = 'accounts:unclaimed';
const ACCOUNTS_CLAIMED_PREFIX = 'accounts:id-';
const ACCOUNTS_FOR = (id) => `${ACCOUNTS_CLAIMED_PREFIX}${id}`;
const HEARTBEAT_FOR = (id) => `heartbeat:${id}`;
const HEARTBEAT_EXPIRES = 30; // 2 min in prod?
const CLAIM_DURATION = 10 * 60 * 1000; // 2 hours on prod?
const PubsubConnector = require('./pubsub-connector')
const forEachAccountList = (forEachCallback) => {
const client = PubsubConnector.broadcastClient();
return Promise.each(client.keysAsync(`accounts:*`), (key) => {
const processId = key.replace('accounts:', '');
return client.lrangeAsync(key, 0, 20000).then((foundIds) =>
forEachCallback(processId, foundIds)
)
});
}
module.exports = {
ACCOUNTS_UNCLAIMED,
ACCOUNTS_CLAIMED_PREFIX,
ACCOUNTS_FOR,
HEARTBEAT_FOR,
HEARTBEAT_EXPIRES,
CLAIM_DURATION,
forEachAccountList,
}

View file

@ -1,7 +1,7 @@
class SyncPolicy {
static defaultPolicy() {
return {
afterSync: 'close',
afterSync: 'idle',
interval: 120 * 1000,
folderSyncOptions: {
deepFolderScan: 10 * 60 * 1000,

View file

@ -0,0 +1,75 @@
const Hapi = require('hapi');
const HapiWebSocket = require('hapi-plugin-websocket');
const Inert = require('inert');
const {DatabaseConnector, PubsubConnector, SchedulerUtils} = require(`nylas-core`);
const {forEachAccountList} = SchedulerUtils;
global.Promise = require('bluebird');
const server = new Hapi.Server();
server.connection({ port: process.env.PORT || 5101 });
DatabaseConnector.forShared().then(({Account}) => {
server.register([HapiWebSocket, Inert], () => {
server.route({
method: "POST",
path: "/accounts",
config: {
plugins: {
websocket: {
only: true,
connect: (wss, ws) => {
Account.findAll().then((accts) => {
accts.forEach((acct) => {
ws.send(JSON.stringify({ cmd: "ACCOUNT", payload: acct }));
});
});
this.redis = PubsubConnector.buildClient();
this.redis.on('pmessage', (pattern, channel) => {
Account.find({where: {id: channel.replace('a-', '')}}).then((acct) => {
ws.send(JSON.stringify({ cmd: "ACCOUNT", payload: acct }));
});
});
this.redis.psubscribe(PubsubConnector.channelForAccount('*'));
this.assignmentsInterval = setInterval(() => {
const assignments = {};
forEachAccountList((identity, accountIds) => {
for (const accountId of accountIds) {
assignments[accountId] = identity;
}
}).then(() =>
ws.send(JSON.stringify({ cmd: "ASSIGNMENTS", payload: assignments}))
)
}, 1000);
},
disconnect: () => {
clearInterval(this.assignmentsInterval);
this.redis.quit();
},
},
},
},
handler: (request, reply) => {
if (request.payload.cmd === "PING") {
reply(JSON.stringify({ result: "PONG" }));
return;
}
},
});
server.route({
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: 'public',
},
},
});
server.start((startErr) => {
if (startErr) { throw startErr; }
console.log('Server running at:', server.info.uri);
});
});
});

View file

@ -0,0 +1,17 @@
{
"name": "nylas-dashboard",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"hapi": "^13.4.1",
"hapi-plugin-websocket": "^0.9.2",
"inert": "^4.0.0",
"nylas-core": "0.x.x"
}
}

View file

@ -0,0 +1,17 @@
body {
background-image: -webkit-linear-gradient(top, rgba(232, 244, 250, 0.6), rgba(231, 231, 233, 1)), url(http://news.nationalgeographic.com/content/dam/news/2015/12/13/BookTalk%20K2/01BookTalkK2.jpg);
background-size: cover;
font-family: sans-serif;
}
.account {
display:inline-block;
width: 300px;
height: 100px;
background-color: white;
padding:15px;
}
.account h3 {
margin: 0; padding: 0;
}

View file

@ -0,0 +1,13 @@
<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="/js/app.jsx" type="text/babel"></script>
<link rel='stylesheet' type="text/css" href="./css/app.css" />
</head>
<body>
<h2>Dashboard</h2>
<div id="root"></div>
</body>
</html>

View file

@ -0,0 +1,91 @@
/* eslint react/react-in-jsx-scope: 0*/
class Account extends React.Component {
propTypes: {
account: React.PropTypes.object,
assignment: React.PropTypes.string,
}
render() {
const {account, assignment} = this.props;
return (
<div className="account">
<h3>{account.email_address}</h3>
<strong>{assignment}</strong>
<div>Sync Interval: {account.sync_policy.interval}ms</div>
<div>Sync Idle Behavior: {account.sync_policy.afterSync}</div>
</div>
);
}
}
class Root extends React.Component {
constructor() {
super();
this.state = {
accounts: {},
assignments: {},
};
}
componentDidMount() {
let url = null;
if (window.location.protocol === "https:") {
url = `wss://${window.location.host}/accounts`;
} else {
url = `ws://${window.location.host}/accounts`;
}
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 === 'ACCOUNT') {
this.onReceivedAccount(msg.payload);
}
if (msg.cmd === 'ASSIGNMENTS') {
this.onReceivedAssignments(msg.payload);
}
} catch (err) {
console.error(err);
}
};
this.websocket.onclose = () => {
window.location.reload();
};
}
onReceivedAssignments(assignments) {
this.setState({assignments})
}
onReceivedAccount(account) {
const accounts = Object.assign({}, this.state.accounts);
accounts[account.id] = account;
this.setState({accounts});
}
render() {
return (
<div>
{
Object.keys(this.state.accounts).sort((a, b) => a.compare(b)).map((key) =>
<Account
key={key}
assignment={this.state.assignments[key]}
account={this.state.accounts[key]}
/>
)
}
</div>
)
}
}
ReactDOM.render(
<Root />,
document.getElementById('root')
);

View file

@ -0,0 +1,42 @@
/**
* 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

@ -0,0 +1,12 @@
/**
* 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});

19599
packages/nylas-dashboard/public/js/react.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,8 @@
const {DatabaseConnector} = require(`nylas-core`)
const {processors} = require('./processors')
global.Promise = require('bluebird');
// List of the attributes of Message that the processor should be allowed to change.
// The message may move between folders, get starred, etc. while it's being
// processed, and it shouldn't overwrite changes to those fields.

View file

@ -1,16 +1,18 @@
const os = require('os');
const SyncWorker = require('./sync-worker');
const {DatabaseConnector, PubsubConnector} = require(`nylas-core`)
const {DatabaseConnector, PubsubConnector, SchedulerUtils} = require(`nylas-core`)
const CPU_COUNT = os.cpus().length;
const IDENTITY = `${os.hostname()}-${process.pid}`;
const ACCOUNTS_UNCLAIMED = 'accounts:unclaimed';
const ACCOUNTS_CLAIMED_PREFIX = 'accounts:id-';
const ACCOUNTS_FOR = (id) => `${ACCOUNTS_CLAIMED_PREFIX}${id}`;
const HEARTBEAT_FOR = (id) => `heartbeat:${id}`;
const HEARTBEAT_EXPIRES = 30; // 2 min in prod?
const CLAIM_DURATION = 10 * 60 * 1000; // 2 hours on prod?
const {
ACCOUNTS_FOR,
ACCOUNTS_UNCLAIMED,
ACCOUNTS_CLAIMED_PREFIX,
HEARTBEAT_FOR,
HEARTBEAT_EXPIRES,
CLAIM_DURATION,
forEachAccountList,
} = SchedulerUtils;
/*
Accounts ALWAYS exist in either `accounts:unclaimed` or an `accounts:{id}` list.
@ -61,7 +63,7 @@ class SyncProcessManager {
client.setAsync(key, Date.now()).then(() =>
client.expireAsync(key, HEARTBEAT_EXPIRES)
).then(() =>
console.log("ProcessManager: Published heartbeat.")
console.log("ProcessManager: ")
)
}
@ -85,11 +87,10 @@ class SyncProcessManager {
console.log("ProcessManager: Starting scan for accountIds in database that are not present in Redis.")
return Promise.each(client.keysAsync(`accounts:*`), (key) =>
client.lrangeAsync(key, 0, 20000).then((foundIds) => {
unseenIds = unseenIds.filter((a) => !foundIds.includes(`${a}`))
})
).finally(() => {
return forEachAccountList((foundProcessIdentity, foundIds) => {
unseenIds = unseenIds.filter((a) => !foundIds.includes(`${a}`))
})
.finally(() => {
if (unseenIds.length === 0) {
return;
}

View file

@ -158,9 +158,9 @@ class SyncWorker {
.then(this.fetchCategoryList.bind(this))
.then(this.syncbackMessageActions.bind(this))
.then(this.fetchMessagesInCategory.bind(this))
.then(() => { this._lastSyncTime = Date.now() })
.catch(console.error)
.finally(() => {
this._lastSyncTime = Date.now()
this.onSyncDidComplete();
this.scheduleNextSync();
});