Add manual auto-update fallback
This commit is contained in:
@@ -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<Props, State> {
|
||||
public render() {
|
||||
const { settingsVisible } = this.props
|
||||
const { content, contentShift, centerContent } = this.getStyles()
|
||||
return <div style={centerContent}>
|
||||
<CssBaseline />
|
||||
<Settings />
|
||||
<div style={settingsVisible ? contentShift : content}>
|
||||
<TitleBar />
|
||||
<div style={centerContent}>
|
||||
<div style={this.getStyles().left}>
|
||||
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
|
||||
this.setState({ selectedNode: node })
|
||||
}} />
|
||||
return (
|
||||
<div style={centerContent}>
|
||||
<CssBaseline />
|
||||
<Settings />
|
||||
<div style={settingsVisible ? contentShift : content}>
|
||||
<TitleBar />
|
||||
<div style={centerContent}>
|
||||
<div style={this.getStyles().left}>
|
||||
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
|
||||
this.setState({ selectedNode: node })
|
||||
}} />
|
||||
</div>
|
||||
<div style={this.getStyles().right}>
|
||||
<Sidebar connectionId={this.state.connectionId} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={this.getStyles().right}>
|
||||
<Sidebar connectionId={this.state.connectionId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
|
||||
</div >
|
||||
</div>
|
||||
<UpdateNotifier />
|
||||
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
212
app/src/UpdateNotifier.tsx
Normal file
212
app/src/UpdateNotifier.tsx
Normal file
@@ -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<Props, {}> {
|
||||
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 (
|
||||
<div>
|
||||
{this.renderUpdateNotification()}
|
||||
{this.renderUpdateDetails()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderUpdateNotification() {
|
||||
const snackbarAnchor = {
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={snackbarAnchor}
|
||||
open={this.props.showUpdateNotification}
|
||||
autoHideDuration={6000}
|
||||
onClose={this.closeNotification}
|
||||
>
|
||||
<SnackbarContent
|
||||
className={this.props.classes.success}
|
||||
message="Update available"
|
||||
action={this.notificationActions()}
|
||||
/>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
||||
|
||||
private notificationActions() {
|
||||
return [(
|
||||
<Button key="undo" size="small" onClick={this.showDetails}>
|
||||
Download
|
||||
</Button>
|
||||
), (
|
||||
<IconButton
|
||||
key="close"
|
||||
aria-label="Close"
|
||||
color="inherit"
|
||||
className={this.props.classes.close}
|
||||
onClick={this.closeNotification}
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
private renderUpdateDetails() {
|
||||
if (!this.updateInfo) {
|
||||
return null
|
||||
}
|
||||
const releaseNotes = (this.updateInfo.releaseNotes as string) || ''
|
||||
return (
|
||||
<Modal
|
||||
open={this.props.showUpdateDetails}
|
||||
disableAutoFocus={true}
|
||||
onClose={this.hideDetails}
|
||||
>
|
||||
<Paper className={this.props.classes.root}>
|
||||
<Typography variant="h6" className={this.props.classes.title}>Version {this.updateInfo.version}</Typography>
|
||||
<Typography className={this.props.classes.title}>Changelog</Typography>
|
||||
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
|
||||
{this.renderDownloads(this.updateInfo)}
|
||||
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>Close</Button>
|
||||
</Paper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
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) => (
|
||||
<div key={index}>
|
||||
<Button
|
||||
className={this.props.classes.download}
|
||||
href={file.url}
|
||||
>
|
||||
<IconButton><CloudDownload /></IconButton>{this.urlToFilename(file.url)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
15
app/src/actions/UpdateNotifier.ts
Normal file
15
app/src/actions/UpdateNotifier.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -18,6 +18,7 @@ const initialAppState: AppState = {
|
||||
},
|
||||
sidebar: {},
|
||||
selectedTopic: undefined,
|
||||
showUpdateDetails: false,
|
||||
}
|
||||
const store = createStore(reducers, initialAppState)
|
||||
|
||||
|
||||
@@ -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<AppState | undefined, CustomAction> = (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<AppState | undefined, CustomAction> = (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
|
||||
}
|
||||
|
||||
@@ -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<any>} = {}
|
||||
|
||||
public manageConnections() {
|
||||
@@ -49,5 +59,16 @@ class ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
const connectionManager = new ConnectionManager()
|
||||
connectionManager.manageConnections()
|
||||
class UpdateNotifier {
|
||||
public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>(this)
|
||||
constructor() {
|
||||
backendEvents.subscribe(checkForUpdates, () => {
|
||||
this.onCheckUpdateRequest.dispatch()
|
||||
})
|
||||
}
|
||||
public notify(updateInfo: UpdateInfo) {
|
||||
backendEvents.emit(updateAvailable, updateInfo)
|
||||
}
|
||||
}
|
||||
|
||||
export const updateNotifier = new UpdateNotifier()
|
||||
|
||||
66
electron.js
66
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: '<ul><li>some</li><li>stuff</li></ul>',
|
||||
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.
|
||||
|
||||
@@ -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<MessageType> {
|
||||
topic: string
|
||||
@@ -23,6 +27,14 @@ export function makeConnectionStateEvent(connectionId: string): Event<DataSource
|
||||
}
|
||||
}
|
||||
|
||||
export const checkForUpdates: Event<void> = {
|
||||
topic: 'app/update/check',
|
||||
}
|
||||
|
||||
export const updateAvailable: Event<UpdateInfo> = {
|
||||
topic: 'app/update/available',
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
topic: string,
|
||||
payload: any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "MQTT-Explorer",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "Explore your message queues",
|
||||
"main": "electron.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user