cncjs
465 строк · 14.3 Кб
1import classNames from 'classnames';2import React, { PureComponent } from 'react';3import { Nav, Navbar, NavDropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap';4import { withRouter } from 'react-router-dom';5import semver from 'semver';6import without from 'lodash/without';7import Push from 'push.js';8import api from 'app/api';9import Anchor from 'app/components/Anchor';10import Space from 'app/components/Space';11import settings from 'app/config/settings';12import combokeys from 'app/lib/combokeys';13import controller from 'app/lib/controller';14import i18n from 'app/lib/i18n';15import log from 'app/lib/log';16import * as user from 'app/lib/user';17import store from 'app/store';18import QuickAccessToolbar from './QuickAccessToolbar';19import styles from './index.styl';20
21const releases = 'https://github.com/cncjs/cncjs/releases';22
23const newUpdateAvailableTooltip = () => {24return (25<Tooltip26id="navbarBrandTooltip"27style={{ color: '#fff' }}28>29<div>{i18n._('New update available')}</div>30</Tooltip>31);32};33
34class Header extends PureComponent {35static propTypes = {36...withRouter.propTypes37};38
39state = this.getInitialState();40
41actions = {42requestPushPermission: () => {43const onGranted = () => {44this.setState({ pushPermission: Push.Permission.GRANTED });45};46const onDenied = () => {47this.setState({ pushPermission: Push.Permission.DENIED });48};49// Note that if "Permission.DEFAULT" is returned, no callback is executed50const permission = Push.Permission.request(onGranted, onDenied);51if (permission === Push.Permission.DEFAULT) {52this.setState({ pushPermission: Push.Permission.DEFAULT });53}54},55checkForUpdates: async () => {56try {57const res = await api.getState();58const { checkForUpdates } = res.body;59
60if (checkForUpdates) {61const res = await api.getLatestVersion();62const { time, version } = res.body;63
64this._isMounted && this.setState({65latestVersion: version,66latestTime: time67});68}69} catch (res) {70// Ignore error71}72},73fetchCommands: async () => {74try {75const res = await api.commands.fetch({ paging: false });76const { records: commands } = res.body;77
78this._isMounted && this.setState({79commands: commands.filter(command => command.enabled)80});81} catch (res) {82// Ignore error83}84},85runCommand: async (cmd) => {86try {87const res = await api.commands.run(cmd.id);88const { taskId } = res.body;89
90this.setState({91commands: this.state.commands.map(c => {92return (c.id === cmd.id) ? { ...c, taskId: taskId, err: null } : c;93})94});95} catch (res) {96// Ignore error97}98}99};100
101actionHandlers = {102CONTROLLER_COMMAND: (event, { command }) => {103// feedhold, cyclestart, homing, unlock, reset104controller.command(command);105}106};107
108controllerEvents = {109'config:change': () => {110this.actions.fetchCommands();111},112'task:start': (taskId) => {113this.setState({114runningTasks: this.state.runningTasks.concat(taskId)115});116},117'task:finish': (taskId, code) => {118const err = (code !== 0) ? new Error(`errno=${code}`) : null;119let cmd = null;120
121this.setState({122commands: this.state.commands.map(c => {123if (c.taskId !== taskId) {124return c;125}126cmd = c;127return {128...c,129taskId: null,130err: err131};132}),133runningTasks: without(this.state.runningTasks, taskId)134});135
136if (cmd && this.state.pushPermission === Push.Permission.GRANTED) {137Push.create(cmd.title, {138body: code === 0139? i18n._('Command succeeded')140: i18n._('Command failed ({{err}})', { err: err }),141icon: 'images/logo-badge-32x32.png',142timeout: 10 * 1000,143onClick: function () {144window.focus();145this.close();146}147});148}149},150'task:error': (taskId, err) => {151let cmd = null;152
153this.setState({154commands: this.state.commands.map(c => {155if (c.taskId !== taskId) {156return c;157}158cmd = c;159return {160...c,161taskId: null,162err: err163};164}),165runningTasks: without(this.state.runningTasks, taskId)166});167
168if (cmd && this.state.pushPermission === Push.Permission.GRANTED) {169Push.create(cmd.title, {170body: i18n._('Command failed ({{err}})', { err: err }),171icon: 'images/logo-badge-32x32.png',172timeout: 10 * 1000,173onClick: function () {174window.focus();175this.close();176}177});178}179}180};181
182_isMounted = false;183
184getInitialState() {185let pushPermission = '';186try {187// Push.Permission.get() will throw an error if Push is not supported on this device188pushPermission = Push.Permission.get();189} catch (e) {190// Ignore191}192
193return {194pushPermission: pushPermission,195commands: [],196runningTasks: [],197currentVersion: settings.version,198latestVersion: settings.version199};200}201
202componentDidMount() {203this._isMounted = true;204
205this.addActionHandlers();206this.addControllerEvents();207
208// Initial actions209this.actions.checkForUpdates();210this.actions.fetchCommands();211}212
213componentWillUnmount() {214this._isMounted = false;215
216this.removeActionHandlers();217this.removeControllerEvents();218
219this.runningTasks = [];220}221
222addActionHandlers() {223Object.keys(this.actionHandlers).forEach(eventName => {224const callback = this.actionHandlers[eventName];225combokeys.on(eventName, callback);226});227}228
229removeActionHandlers() {230Object.keys(this.actionHandlers).forEach(eventName => {231const callback = this.actionHandlers[eventName];232combokeys.removeListener(eventName, callback);233});234}235
236addControllerEvents() {237Object.keys(this.controllerEvents).forEach(eventName => {238const callback = this.controllerEvents[eventName];239controller.addListener(eventName, callback);240});241}242
243removeControllerEvents() {244Object.keys(this.controllerEvents).forEach(eventName => {245const callback = this.controllerEvents[eventName];246controller.removeListener(eventName, callback);247});248}249
250render() {251const { history, location } = this.props;252const { pushPermission, commands, runningTasks, currentVersion, latestVersion } = this.state;253const newUpdateAvailable = semver.lt(currentVersion, latestVersion);254const tooltip = newUpdateAvailable ? newUpdateAvailableTooltip() : <div />;255const sessionEnabled = store.get('session.enabled');256const signedInName = store.get('session.name');257const hideUserDropdown = !sessionEnabled;258const showCommands = commands.length > 0;259
260return (261<Navbar262fixedTop
263fluid
264inverse
265style={{266border: 'none',267margin: 0268}}269>270<Navbar.Header>271<OverlayTrigger272overlay={tooltip}273placement="right"274>275<Anchor276className="navbar-brand"277style={{278padding: 0,279position: 'relative',280height: 50,281width: 60282}}283href={releases}284target="_blank"285title={`${settings.productName} ${settings.version}`}286>287<img288style={{289margin: '4px auto 0 auto'290}}291src="images/logo-badge-32x32.png"292alt=""293/>294<div295style={{296fontSize: '50%',297lineHeight: '14px',298textAlign: 'center',299whiteSpace: 'nowrap',300}}301>302{settings.version}303</div>304{newUpdateAvailable && (305<span306className="label label-primary"307style={{308fontSize: '50%',309position: 'absolute',310top: 2,311right: 2312}}313>314N315</span>316)}317</Anchor>318</OverlayTrigger>319<Navbar.Toggle />320</Navbar.Header>321<Navbar.Collapse>322<Nav pullRight>323<NavDropdown324className={classNames(325{ 'hidden': hideUserDropdown }326)}327id="nav-dropdown-user"328title={(329<div title={i18n._('My Account')}>330<i className="fa fa-fw fa-user" />331</div>332)}333noCaret
334>335<MenuItem header>336{i18n._('Signed in as {{name}}', { name: signedInName })}337</MenuItem>338<MenuItem divider />339<MenuItem340href="#/settings/user-accounts"341>342<i className="fa fa-fw fa-user" />343<Space width="8" />344{i18n._('Account')}345</MenuItem>346<MenuItem347onClick={() => {348if (user.isAuthenticated()) {349log.debug('Destroy and cleanup the WebSocket connection');350controller.disconnect();351
352user.signout();353
354// Remember current location355history.replace(location.pathname);356}357}}358>359<i className="fa fa-fw fa-sign-out" />360<Space width="8" />361{i18n._('Sign Out')}362</MenuItem>363</NavDropdown>364<NavDropdown365id="nav-dropdown-menu"366title={(367<div title={i18n._('Options')}>368<i className="fa fa-fw fa-ellipsis-v" />369{this.state.runningTasks.length > 0 && (370<span371className="label label-primary"372style={{373position: 'absolute',374top: 4,375right: 4376}}377>378N379</span>380)}381</div>382)}383noCaret
384>385{showCommands && (386<MenuItem header>387{i18n._('Command')}388{pushPermission === Push.Permission.GRANTED && (389<span className="pull-right">390<i className="fa fa-fw fa-bell-o" />391</span>392)}393{pushPermission === Push.Permission.DENIED && (394<span className="pull-right">395<i className="fa fa-fw fa-bell-slash-o" />396</span>397)}398{pushPermission === Push.Permission.DEFAULT && (399<span className="pull-right">400<Anchor401className={styles.btnIcon}402onClick={this.actions.requestPushPermission}403title={i18n._('Show notifications')}404>405<i className="fa fa-fw fa-bell" />406</Anchor>407</span>408)}409</MenuItem>410)}411{showCommands && commands.map((cmd) => {412const isTaskRunning = runningTasks.indexOf(cmd.taskId) >= 0;413
414return (415<MenuItem416key={cmd.id}417disabled={cmd.disabled}418onSelect={() => {419this.actions.runCommand(cmd);420}}421>422<span title={cmd.command}>{cmd.title || cmd.command}</span>423<span className="pull-right">424<i425className={classNames(426'fa',427'fa-fw',428{ 'fa-circle-o-notch': isTaskRunning },429{ 'fa-spin': isTaskRunning },430{ 'fa-exclamation-circle': cmd.err },431{ 'text-error': cmd.err }432)}433title={cmd.err}434/>435</span>436</MenuItem>437);438})}439{showCommands &&440<MenuItem divider />441}442<MenuItem443href="https://github.com/cncjs/cncjs/wiki"444target="_blank"445>446{i18n._('Help')}447</MenuItem>448<MenuItem449href="https://github.com/cncjs/cncjs/issues"450target="_blank"451>452{i18n._('Report an issue')}453</MenuItem>454</NavDropdown>455</Nav>456{location.pathname === '/workspace' &&457<QuickAccessToolbar state={this.state} actions={this.actions} />458}459</Navbar.Collapse>460</Navbar>461);462}463}
464
465export default withRouter(Header);466