diff --git a/app/src/App.tsx b/app/src/App.tsx index 23bd9f1..7205cc8 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,14 +1,17 @@ import * as React from 'react' -import { connect } from 'react-redux' import * as q from '../../backend/src/Model' -import CssBaseline from '@material-ui/core/CssBaseline' -import { withStyles, Theme } from '@material-ui/core/styles' -import Tree from './components/Tree/Tree' -import TitleBar from './components/TitleBar' -import Sidebar from './components/Sidebar/Sidebar' -import Connection from './components/ConnectionSetup/Connection' -import Settings from './components/Settings' + +import { Theme, withStyles } from '@material-ui/core/styles' + import { AppState } from './reducers' +import Connection from './components/ConnectionSetup/Connection' +import CssBaseline from '@material-ui/core/CssBaseline' +import Settings from './components/Settings' +import Sidebar from './components/Sidebar/Sidebar' +import TitleBar from './components/TitleBar' +import Tree from './components/Tree/Tree' +import UpdateNotifier from './UpdateNotifier' +import { connect } from 'react-redux' interface State { selectedNode?: q.TreeNode, @@ -79,24 +82,27 @@ class App extends React.Component { public render() { const { settingsVisible } = this.props const { content, contentShift, centerContent } = this.getStyles() - return
- - -
- -
-
- { - this.setState({ selectedNode: node }) - }} /> + return ( +
+ + +
+ +
+
+ { + this.setState({ selectedNode: node }) + }} /> +
+
+ +
-
- -
-
-
- this.setState({ connectionId })}/> -
+
+ + this.setState({ connectionId })}/> +
+ ) } } diff --git a/app/src/UpdateNotifier.tsx b/app/src/UpdateNotifier.tsx new file mode 100644 index 0000000..94b1d59 --- /dev/null +++ b/app/src/UpdateNotifier.tsx @@ -0,0 +1,212 @@ +import * as React from 'react' + +import { + Button, + IconButton, + Modal, + Paper, + Snackbar, + SnackbarContent, + Typography, +} from '@material-ui/core' +import { Theme, withStyles } from '@material-ui/core/styles' +import { UpdateInfo, checkForUpdates, rendererEvents, updateAvailable } from '../../events' +import { green, red } from '@material-ui/core/colors' + +import { AppState } from './reducers' +import Close from '@material-ui/icons/Close' +import CloudDownload from '@material-ui/icons/CloudDownload' +import { UpdateFileInfo } from 'builder-util-runtime' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { updateNotifierActions } from './actions' + +interface Props { + showUpdateNotification: boolean + showUpdateDetails: boolean + classes: any + actions: any +} + +class UpdateNotifier extends React.Component { + private updateInfo?: UpdateInfo + constructor(props: any) { + super(props) + this.state = { + selectedNode: undefined, + } + } + + public componentDidMount() { + rendererEvents.emit(checkForUpdates, undefined) + rendererEvents.subscribe(updateAvailable, this.handleUpdate) + } + + public componentWillUnmount() { + rendererEvents.unsubscribeAll(updateAvailable) + } + + private handleUpdate = (updateInfo: UpdateInfo) => { + this.updateInfo = updateInfo + this.props.actions.showUpdateNotification(true) + } + + private closeNotification = () => { + this.props.actions.showUpdateNotification(false) + } + + private showDetails = () => { + this.closeNotification() + this.props.actions.showUpdateDetails(true) + } + + private hideDetails = () => { + this.props.actions.showUpdateDetails(false) + } + + public render() { + return ( +
+ {this.renderUpdateNotification()} + {this.renderUpdateDetails()} +
+ ) + } + + private renderUpdateNotification() { + const snackbarAnchor = { + vertical: 'top', + horizontal: 'right', + } + + return ( + + + + ) + } + + private notificationActions() { + return [( + + ), ( + + + + ), + ] + } + + private renderUpdateDetails() { + if (!this.updateInfo) { + return null + } + const releaseNotes = (this.updateInfo.releaseNotes as string) || '' + return ( + + + Version {this.updateInfo.version} + Changelog +
+ {this.renderDownloads(this.updateInfo)} + + + + ) + } + + private urlToFilename(url: string) { + const parts = url.split('/') + + return parts[parts.length - 1] + } + + private renderDownloads(updateInfo: UpdateInfo) { + console.log(updateInfo) + return updateInfo.files + .map((file: UpdateFileInfo, index: number) => ( +
+ +
+ )) + } +} + +const styles = (theme: Theme) => ({ + success: { + backgroundColor: green[600], + color: theme.typography.button.color, + }, + close: { + padding: theme.spacing.unit / 2, + }, + root: { + minWidth: '350px', + maxWidth: '500px', + backgroundColor: theme.palette.background.default, + margin: '20vh auto auto auto', + padding: `${2 * theme.spacing.unit}px`, + outline: 'none', + }, + title: { + color: theme.palette.text.primary, + }, + releaseNotes: { + overflow: 'auto scroll', + color: theme.palette.text.secondary, + backgroundColor: 'rgba(60, 60, 60, 0.6)', + maxHeight: '28vh', + }, + paper: { + padding: `${theme.spacing.unit * 2}px`, + color: theme.palette.text.secondary, + }, + download: { + width: '100%', + }, + closeButton: { + display: 'block', + margin: '0 0 0 auto', + }, +}) + +const mapStateToProps = (state: AppState) => { + return { + showUpdateNotification: state.showUpdateNotification, + showUpdateDetails: state.showUpdateDetails, + } +} + +const mapDispatchToProps = (dispatch: any) => { + return { + actions: bindActionCreators(updateNotifierActions, dispatch), + } +} + +export default withStyles(styles, { withTheme: true })(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier)) diff --git a/app/src/actions/UpdateNotifier.ts b/app/src/actions/UpdateNotifier.ts new file mode 100644 index 0000000..19c3400 --- /dev/null +++ b/app/src/actions/UpdateNotifier.ts @@ -0,0 +1,15 @@ +import { ActionTypes, CustomAction } from '../reducers' + +export const showUpdateNotification = (show: boolean): CustomAction => { + return { + type: ActionTypes.showUpdateNotification, + showUpdateNotification: show, + } +} + +export const showUpdateDetails = (show: boolean): CustomAction => { + return { + type: ActionTypes.showUpdateDetails, + showUpdateDetails: show, + } +} diff --git a/app/src/actions/index.ts b/app/src/actions/index.ts index d17ba68..6729100 100644 --- a/app/src/actions/index.ts +++ b/app/src/actions/index.ts @@ -1,5 +1,6 @@ import * as settingsActions from './Settings' -import * as treeActions from './Tree' import * as sidebarActions from './Sidebar' +import * as treeActions from './Tree' +import * as updateNotifierActions from './UpdateNotifier' -export { settingsActions, treeActions, sidebarActions } +export { settingsActions, treeActions, sidebarActions, updateNotifierActions } diff --git a/app/src/index.tsx b/app/src/index.tsx index 0432f17..97554d1 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -18,6 +18,7 @@ const initialAppState: AppState = { }, sidebar: {}, selectedTopic: undefined, + showUpdateDetails: false, } const store = createStore(reducers, initialAppState) diff --git a/app/src/reducers/index.ts b/app/src/reducers/index.ts index f3fba97..ee7d045 100644 --- a/app/src/reducers/index.ts +++ b/app/src/reducers/index.ts @@ -11,6 +11,8 @@ export enum ActionTypes { selectTopic = 'SELECT_TOPIC', setPublishTopic = 'SET_PUBLISH_TOPIC', setPublishPayload = 'SET_PUBLISH_PAYLOAD', + showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', + showUpdateDetails = 'SHOW_UPDATE_DETAILS', } export interface CustomAction extends Action { @@ -20,6 +22,8 @@ export interface CustomAction extends Action { selectedTopic?: q.TreeNode publishTopic?: string publishPayload?: string + showUpdateNotification?: boolean + showUpdateDetails?: boolean } export interface SidebarState { @@ -31,6 +35,8 @@ export interface AppState { settings: SettingsState, selectedTopic?: q.TreeNode sidebar: SidebarState + showUpdateNotification?: boolean + showUpdateDetails: boolean } export interface SettingsState { @@ -51,7 +57,7 @@ const reducer: Reducer = (state, action) => throw Error('No initial state') } trackEvent(action.type) - + console.log(action, state) switch (action.type) { case ActionTypes.setAutoExpandLimit: if (action.autoExpandLimit === undefined) { @@ -98,6 +104,19 @@ const reducer: Reducer = (state, action) => ...state, settings: { ...state.settings, nodeOrder: action.nodeOrder }, } + case ActionTypes.showUpdateNotification: + return { + ...state, + showUpdateNotification: action.showUpdateNotification, + } + case ActionTypes.showUpdateDetails: + if (action.showUpdateDetails === undefined) { + return state + } + return { + ...state, + showUpdateDetails: action.showUpdateDetails, + } default: return state } diff --git a/backend/src/index.ts b/backend/src/index.ts index d0c3dd0..9e78efb 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,11 +1,21 @@ import { - addMqttConnectionEvent, backendEvents, - makeConnectionStateEvent, removeConnection, - makeConnectionMessageEvent, makePublishEvent, AddMqttConnection, Message, + AddMqttConnection, + EventDispatcher, + Message, + addMqttConnectionEvent, + backendEvents, + checkForUpdates, + makeConnectionMessageEvent, + makeConnectionStateEvent, + makePublishEvent, + removeConnection, + updateAvailable, } from '../../events' -import { MqttSource, DataSource } from './DataSource' +import { DataSource, MqttSource } from './DataSource' -class ConnectionManager { +import { UpdateInfo } from 'builder-util-runtime' + +export class ConnectionManager { private connections: {[s: string]: DataSource} = {} public manageConnections() { @@ -49,5 +59,16 @@ class ConnectionManager { } } -const connectionManager = new ConnectionManager() -connectionManager.manageConnections() +class UpdateNotifier { + public onCheckUpdateRequest = new EventDispatcher(this) + constructor() { + backendEvents.subscribe(checkForUpdates, () => { + this.onCheckUpdateRequest.dispatch() + }) + } + public notify(updateInfo: UpdateInfo) { + backendEvents.emit(updateAvailable, updateInfo) + } +} + +export const updateNotifier = new UpdateNotifier() diff --git a/electron.js b/electron.js index 7ec86b2..036eb3e 100644 --- a/electron.js +++ b/electron.js @@ -1,17 +1,15 @@ +const { app, BrowserWindow } = require('electron') const { autoUpdater } = require("electron-updater") const log = require('electron-log'); +const { ConnectionManager, updateNotifier } = require('./backend/build/backend/src/index.js') autoUpdater.logger = log; autoUpdater.logger.transports.file.level = 'info'; log.info('App starting...'); -// Modules to control application life and create native browser window -const { app, BrowserWindow, Notification } = require('electron') -try { - require('./backend/build/backend/src/index.js') -} catch (err) { - console.error(err) -} +const connectionManager = new ConnectionManager() +connectionManager.manageConnections() + // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow @@ -29,31 +27,6 @@ function createWindow () { // and load the index.html of the app. mainWindow.loadFile('app/index.html') - mainWindow.webContents.once('dom-ready', () => { - - console.log('window loaded, check for updates') - let updateInfo - - autoUpdater.on('update-available', (info) => { - updateInfo = info - }) - - autoUpdater.on('error', () => { - const version = updateInfo ? ` (${updateInfo.version})` : '' - const releaseNotes = ((updateInfo && updateInfo.releaseNotes) ? `${updateInfo.releaseNotes}\n` : '') - let notification = new Notification({ - title: 'Update available' + version, - silent: true, - body: releaseNotes + 'https://github.com/thomasnordquist/MQTT-Explorer/releases' - }) - notification.show() - }) - try { - autoUpdater.checkForUpdatesAndNotify() - } catch (error) { - console.error(error) - } - }) // Open the DevTools. // mainWindow.webContents.openDevTools() @@ -72,6 +45,35 @@ function createWindow () { // Some APIs can only be used after this event occurs. app.on('ready', () => { createWindow() + + let updateInfo + autoUpdater.on('update-available', (info) => { + updateInfo = info + }) + + autoUpdater.on('error', () => { + if (updateInfo) { + updateNotifier.notify(updateInfo) + } + }) + + updateNotifier.onCheckUpdateRequest.subscribe(() => { + updateNotifier.notify({ + version: '0.0.4', + releaseNotes: '
  • some
  • stuff
', + files: [{ + url: 'https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.2/MQTT-Explorer-0.0.2.dmg' + }, + { + url: 'https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.2/MQTT-Explorer-0.0.2-mac.zip' + }] + }) + try { + autoUpdater.checkForUpdatesAndNotify() + } catch (error) { + console.error(error) + } + }) }) // Quit when all windows are closed. diff --git a/events/Events.ts b/events/Events.ts index 9bd3e32..0284ff3 100644 --- a/events/Events.ts +++ b/events/Events.ts @@ -1,4 +1,8 @@ -import { MqttOptions, DataSourceState } from '../backend/src/DataSource' +import { DataSourceState, MqttOptions } from '../backend/src/DataSource' + +import { UpdateInfo } from 'builder-util-runtime' + +export { UpdateInfo } from 'builder-util-runtime' export interface Event { topic: string @@ -23,6 +27,14 @@ export function makeConnectionStateEvent(connectionId: string): Event = { + topic: 'app/update/check', +} + +export const updateAvailable: Event = { + topic: 'app/update/available', +} + export interface Message { topic: string, payload: any diff --git a/package.json b/package.json index abf9479..cf760db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "MQTT-Explorer", - "version": "0.0.2", + "version": "0.0.3", "description": "Explore your message queues", "main": "electron.js", "scripts": {