Add manual auto-update fallback

This commit is contained in:
Thomas Nordquist
2019-01-13 20:49:36 +01:00
parent fdece7ae91
commit e294d9700f
10 changed files with 358 additions and 69 deletions

View File

@@ -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,7 +82,8 @@ class App extends React.Component<Props, State> {
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent } = this.getStyles()
return <div style={centerContent}>
return (
<div style={centerContent}>
<CssBaseline />
<Settings />
<div style={settingsVisible ? contentShift : content}>
@@ -95,8 +99,10 @@ class App extends React.Component<Props, State> {
</div>
</div>
</div>
<UpdateNotifier />
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
</div >
)
}
}

212
app/src/UpdateNotifier.tsx Normal file
View 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))

View 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,
}
}

View File

@@ -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 }

View File

@@ -18,6 +18,7 @@ const initialAppState: AppState = {
},
sidebar: {},
selectedTopic: undefined,
showUpdateDetails: false,
}
const store = createStore(reducers, initialAppState)

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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.

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "MQTT-Explorer",
"version": "0.0.2",
"version": "0.0.3",
"description": "Explore your message queues",
"main": "electron.js",
"scripts": {