mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-22 08:16:09 +08:00
Dashboard with a sweet background. Also realtime assignment / policy view.
This commit is contained in:
parent
3f5cac4342
commit
09bb7874f8
|
@ -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 });
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const Imap = require('imap');
|
||||
const EventEmitter = require('events');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const Capabilities = {
|
||||
Gmail: 'X-GM-EXT-1',
|
||||
|
|
|
@ -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'}`),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
29
packages/nylas-core/scheduler-utils.js
Normal file
29
packages/nylas-core/scheduler-utils.js
Normal 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,
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
class SyncPolicy {
|
||||
static defaultPolicy() {
|
||||
return {
|
||||
afterSync: 'close',
|
||||
afterSync: 'idle',
|
||||
interval: 120 * 1000,
|
||||
folderSyncOptions: {
|
||||
deepFolderScan: 10 * 60 * 1000,
|
||||
|
|
75
packages/nylas-dashboard/app.js
Normal file
75
packages/nylas-dashboard/app.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
17
packages/nylas-dashboard/package.json
Normal file
17
packages/nylas-dashboard/package.json
Normal 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"
|
||||
}
|
||||
}
|
17
packages/nylas-dashboard/public/css/app.css
Normal file
17
packages/nylas-dashboard/public/css/app.css
Normal 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;
|
||||
}
|
13
packages/nylas-dashboard/public/index.html
Normal file
13
packages/nylas-dashboard/public/index.html
Normal 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>
|
91
packages/nylas-dashboard/public/js/app.jsx
Normal file
91
packages/nylas-dashboard/public/js/app.jsx
Normal 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')
|
||||
);
|
42
packages/nylas-dashboard/public/js/react-dom.js
vendored
Normal file
42
packages/nylas-dashboard/public/js/react-dom.js
vendored
Normal 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;
|
||||
});
|
12
packages/nylas-dashboard/public/js/react-dom.min.js
vendored
Normal file
12
packages/nylas-dashboard/public/js/react-dom.min.js
vendored
Normal 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
19599
packages/nylas-dashboard/public/js/react.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
16
packages/nylas-dashboard/public/js/react.min.js
vendored
Normal file
16
packages/nylas-dashboard/public/js/react.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue