Skip to content

Commit

Permalink
Send push notifications (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
cheton committed Feb 6, 2017
1 parent bb5f74b commit 7c8fb6f
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 49 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"package-json": "~2.4.0",
"parse-json": "~2.2.0",
"pubsub-js": "~1.5.4",
"push.js": "~0.0.12",
"range_check": "~1.4.0",
"rc-slider": "~6.0.1",
"rc-table": "~5.2.8",
Expand Down
17 changes: 11 additions & 6 deletions src/app/api/api.commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ const state = {

const mapConfigToState = (config) => {
state.commands = _.map(config.commands, (c) => {
return { ...c, id: uuid.v4() };
return {
id: uuid.v4(),
title: c.title || c.text,
command: c.command
};
});
};

Expand All @@ -24,11 +28,12 @@ configStore.on('change', mapConfigToState);

export const getCommands = (req, res) => {
res.send({
commands: state.commands.map(({ id, command, text }) => {
commands: state.commands.map(({ id, title, command }) => {
return {
id: id,
disabled: !command,
text: text
id: id,
title: title,
command: command
};
})
});
Expand All @@ -45,9 +50,9 @@ export const runCommand = (req, res) => {
return;
}

log.info(`${PREFIX} Execute the "${c.text}" command from "${c.command}"`);
log.info(`${PREFIX} Execute the "${c.title}" command from "${c.command}"`);

const taskId = taskRunner.run(c.command);
const taskId = taskRunner.run(c.command, c.title);

res.send({ taskId: taskId });
};
18 changes: 9 additions & 9 deletions src/app/services/cncengine/CNCEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ const PREFIX = '[cncengine]';
class CNCServer {
controllers = store.get('controllers');
listener = {
taskRun: (...args) => {
this.io.sockets.emit('task:run', ...args);
taskStart: (...args) => {
this.io.sockets.emit('task:start', ...args);
},
taskFinish: (...args) => {
this.io.sockets.emit('task:finish', ...args);
},
taskError: (...args) => {
this.io.sockets.emit('task:error', ...args);
},
taskComplete: (...args) => {
this.io.sockets.emit('task:complete', ...args);
},
configChange: (...args) => {
this.io.sockets.emit('config:change', ...args);
},
Expand All @@ -39,9 +39,9 @@ class CNCServer {
start(server) {
this.stop();

taskRunner.on('run', this.listener.taskRun);
taskRunner.on('start', this.listener.taskStart);
taskRunner.on('finish', this.listener.taskFinish);
taskRunner.on('error', this.listener.taskError);
taskRunner.on('complete', this.listener.taskComplete);
config.on('change', this.listener.configChange);
store.on('change', this.listener.storeChange);

Expand Down Expand Up @@ -207,9 +207,9 @@ class CNCServer {
this.sockets = [];
this.server = null;

taskRunner.removeListener('run', this.listener.taskRun);
taskRunner.removeListener('start', this.listener.taskStart);
taskRunner.removeListener('finish', this.listener.taskFinish);
taskRunner.removeListener('error', this.listener.taskError);
taskRunner.removeListener('complete', this.listener.taskComplete);
config.removeListener('change', this.listener.configChange);
store.removeListener('change', this.listener.storeChange);
}
Expand Down
37 changes: 23 additions & 14 deletions src/app/services/taskrunner/TaskRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ const PREFIX = '[taskrunner]';
class TaskRunner extends events.EventEmitter {
tasks = [];

run(command, options = {}) {
const id = shortid.generate(); // task id
run(command, title, options) {
if (options === undefined && typeof title === 'object') {
options = title;
title = '';
}

const taskId = shortid.generate(); // task id
const child = defaultShell.spawn(command, {
detached: true,
...options
});
child.unref();

this.tasks.push(id);
this.emit('run', id);
this.tasks.push(taskId);
this.emit('start', taskId);

child.stdout.on('data', (data) => {
process.stdout.write(`PID:${child.pid}> ${data}`);
Expand All @@ -28,21 +33,25 @@ class TaskRunner extends events.EventEmitter {
});
child.on('error', (err) => {
// Listen for error event can prevent from throwing an unhandled exception
log.error(`${PREFIX} Failed to start child process: err=${JSON.stringify(err)}`);
this.emit('error', id, err);
});
child.on('close', (code) => {
this.tasks = without(this.tasks, id);
log.error(`${PREFIX} Failed to start a child process: err=${JSON.stringify(err)}`);

this.tasks = without(this.tasks, taskId);
this.emit('error', taskId, err);
});
// The 'exit' event is emitted after the child process ends.
// Note that the 'exit' event may or may not fire after an error has occurred.
// It is important to guard against accidentally invoking handler functions multiple times.
child.on('exit', (code) => {
this.tasks = without(this.tasks, id);
this.emit('complete', id, code);
if (this.contains(taskId)) {
this.tasks = without(this.tasks, taskId);
this.emit('finish', taskId, code);
}
});

return id;
return taskId;
}
contains(id) {
return this.tasks.indexOf(id) >= 0;
contains(taskId) {
return this.tasks.indexOf(taskId) >= 0;
}
}

Expand Down
140 changes: 122 additions & 18 deletions src/web/containers/Header/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { Component, PropTypes } from 'react';
import { Nav, Navbar, NavDropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap';
import semver from 'semver';
import without from 'lodash/without';
import Push from 'push.js';
import api from '../../api';
import Anchor from '../../components/Anchor';
import settings from '../../config/settings';
Expand All @@ -11,6 +12,7 @@ import controller from '../../lib/controller';
import i18n from '../../lib/i18n';
import store from '../../store';
import QuickAccessToolbar from './QuickAccessToolbar';
import styles from './index.styl';

const releases = 'https://github.com/cncjs/cncjs/releases';

Expand All @@ -31,12 +33,26 @@ class Header extends Component {
};

state = {
pushPermission: Push.Permission.get(),
commands: [],
runningTasks: [],
currentVersion: settings.version,
latestVersion: settings.version
};
actions = {
requestPushPermission: () => {
const onGranted = () => {
this.setState({ pushPermission: Push.Permission.GRANTED });
};
const onDenied = () => {
this.setState({ pushPermission: Push.Permission.DENIED });
};
// Note that if "Permission.DEFAULT" is returned, no callback is executed
const permission = Push.Permission.request(onGranted, onDenied);
if (permission === Push.Permission.DEFAULT) {
this.setState({ pushPermission: Push.Permission.DEFAULT });
}
},
checkForUpdates: async () => {
try {
const res = await api.getState();
Expand Down Expand Up @@ -72,7 +88,7 @@ class Header extends Component {

this.setState({
commands: this.state.commands.map(c => {
return (c.id === cmd.id) ? { ...c, taskId: taskId } : c;
return (c.id === cmd.id) ? { ...c, taskId: taskId, err: null } : c;
})
});
} catch (res) {
Expand All @@ -81,26 +97,73 @@ class Header extends Component {
}
};
controllerEvents = {
'task:run': (taskId) => {
'task:start': (taskId) => {
this.setState({
runningTasks: this.state.runningTasks.concat(taskId)
});
},
'task:complete': (taskId) => {
'task:finish': (taskId, code) => {
const err = (code !== 0) ? new Error(`errno=${code}`) : null;
let cmd = null;

this.setState({
commands: this.state.commands.map(c => {
return (c.taskId === taskId) ? { ...c, taskId: null } : c;
if (c.taskId !== taskId) {
return c;
}
cmd = c;
return {
...c,
taskId: null,
err: err
};
}),
runningTasks: without(this.state.runningTasks, taskId)
});

if (cmd) {
Push.create(cmd.title, {
body: code === 0
? i18n._('Command succeeded')
: i18n._('Command failed ({{err}})', { err: err }),
icon: 'images/32x32/logo.png',
timeout: 10 * 1000,
onClick: function () {
window.focus();
this.close();
}
});
}
},
'task:error': (taskId) => {
'task:error': (taskId, err) => {
let cmd = null;

this.setState({
commands: this.state.commands.map(c => {
return (c.taskId === taskId) ? { ...c, taskId: null } : c;
if (c.taskId !== taskId) {
return c;
}
cmd = c;
return {
...c,
taskId: null,
err: err
};
}),
runningTasks: without(this.state.runningTasks, taskId)
});

if (cmd) {
Push.create(cmd.title, {
body: i18n._('Command failed ({{err}})', { err: err }),
icon: 'images/32x32/logo.png',
timeout: 10 * 1000,
onClick: function () {
window.focus();
this.close();
}
});
}
},
'config:change': () => {
this.actions.getCommands();
Expand Down Expand Up @@ -142,7 +205,7 @@ class Header extends Component {
}
render() {
const { path } = this.props;
const { commands, runningTasks, currentVersion, latestVersion } = this.state;
const { pushPermission, commands, runningTasks, currentVersion, latestVersion } = this.state;
const newUpdateAvailable = semver.lt(currentVersion, latestVersion);
const tooltip = newUpdateAvailable ? newUpdateAvailableTooltip() : <div />;
const sessionEnabled = store.get('session.enabled');
Expand Down Expand Up @@ -228,12 +291,49 @@ class Header extends Component {
</NavDropdown>
<NavDropdown
id="nav-dropdown-menu"
title={<i className="fa fa-fw fa-ellipsis-v" />}
title={
<div>
<i className="fa fa-fw fa-ellipsis-v" />
{this.state.runningTasks.length > 0 &&
<span
className="label label-primary"
style={{
position: 'absolute',
top: 4,
right: 4
}}
>
N
</span>
}
</div>
}
noCaret
>
{showCommands &&
<MenuItem header>
{i18n._('Command')}
{pushPermission === Push.Permission.GRANTED &&
<span className="pull-right">
<i className="fa fa-fw fa-bell-o" />
</span>
}
{pushPermission === Push.Permission.DENIED &&
<span className="pull-right">
<i className="fa fa-fw fa-bell-slash-o" />
</span>
}
{pushPermission === Push.Permission.DEFAULT &&
<span className="pull-right">
<Anchor
className={styles.btnIcon}
onClick={this.actions.requestPushPermission}
title={i18n._('Show notifications')}
>
<i className="fa fa-fw fa-bell" />
</Anchor>
</span>
}
</MenuItem>
}
{showCommands && commands.map((cmd) => {
Expand All @@ -247,16 +347,20 @@ class Header extends Component {
this.actions.runCommand(cmd);
}}
>
{cmd.text}
<i
className={classNames(
'pull-right',
'fa',
'fa-fw',
{ 'fa-circle-o-notch': isTaskRunning },
{ 'fa-spin': isTaskRunning }
)}
/>
<span title={cmd.command}>{cmd.title || cmd.command}</span>
<span className="pull-right">
<i
className={classNames(
'fa',
'fa-fw',
{ 'fa-circle-o-notch': isTaskRunning },
{ 'fa-spin': isTaskRunning },
{ 'fa-exclamation-circle': cmd.err },
{ 'text-error': cmd.err }
)}
title={cmd.err}
/>
</span>
</MenuItem>
);
})}
Expand Down
8 changes: 8 additions & 0 deletions src/web/containers/Header/index.styl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
}
}

.btn-icon {
color: #444;
&:hover,
&:focus {
color: #222;
}
}

.quick-access-toolbar {
margin: 10px 0 10px 15px;

Expand Down
Binary file added src/web/images/32x32/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7c8fb6f

Please sign in to comment.