Improve settings storage

- add error reporting
- refactor
This commit is contained in:
Thomas Nordquist
2019-02-17 21:02:17 +01:00
parent 0ad91872a1
commit 9207af0aaa
17 changed files with 133 additions and 66 deletions

View File

@@ -2,13 +2,16 @@ import * as React from 'react'
import ConnectionSetup from './components/ConnectionSetup/ConnectionSetup' import ConnectionSetup from './components/ConnectionSetup/ConnectionSetup'
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import ErrorBoundary from './ErrorBoundary' import ErrorBoundary from './ErrorBoundary'
import Notification from './components/Notification'
import Sidebar from './components/Sidebar/Sidebar' import Sidebar from './components/Sidebar/Sidebar'
import TitleBar from './components/TitleBar' import TitleBar from './components/TitleBar'
import Tree from './components/Tree/Tree' import Tree from './components/Tree/Tree'
import UpdateNotifier from './UpdateNotifier' import UpdateNotifier from './UpdateNotifier'
import { AppState } from './reducers' import { AppState } from './reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { default as SplitPane } from 'react-split-pane' import { default as SplitPane } from 'react-split-pane'
import { globalActions } from './actions'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
const Settings = React.lazy(() => import('./components/Settings')) const Settings = React.lazy(() => import('./components/Settings'))
@@ -18,6 +21,8 @@ interface Props {
connectionId: string connectionId: string
classes: any classes: any
settingsVisible: boolean settingsVisible: boolean
error?: string
actions: any
} }
class App extends React.PureComponent<Props, {}> { class App extends React.PureComponent<Props, {}> {
@@ -26,6 +31,18 @@ class App extends React.PureComponent<Props, {}> {
this.state = { } this.state = { }
} }
private renderError() {
if (this.props.error) {
const error = typeof this.props.error === 'string' ? this.props.error : JSON.stringify(this.props.error)
return (
<Notification
message={error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
}
public render() { public render() {
const { settingsVisible } = this.props const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes
@@ -34,6 +51,7 @@ class App extends React.PureComponent<Props, {}> {
<div className={centerContent}> <div className={centerContent}>
<CssBaseline /> <CssBaseline />
<ErrorBoundary> <ErrorBoundary>
{this.renderError()}
<React.Suspense fallback={<div>Loading...</div>}> <React.Suspense fallback={<div>Loading...</div>}>
<Settings /> <Settings />
</React.Suspense> </React.Suspense>
@@ -75,6 +93,7 @@ const mapStateToProps = (state: AppState) => {
return { return {
settingsVisible: state.settings.visible, settingsVisible: state.settings.visible,
connectionId: state.connection.connectionId, connectionId: state.connection.connectionId,
error: state.globalState.error,
} }
} }
@@ -120,4 +139,10 @@ const styles = (theme: Theme) => {
} }
} }
export default withStyles(styles)(connect(mapStateToProps)(App)) const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(globalActions, dispatch),
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))

View File

@@ -1,5 +1,9 @@
import * as React from 'react' import * as React from 'react'
import PersistantStorage from './PersistantStorage'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning'
import { electronRendererTelementry } from 'electron-telemetry' import { electronRendererTelementry } from 'electron-telemetry'
import { Theme, withStyles } from '@material-ui/core/styles'
import { import {
Button, Button,
Modal, Modal,
@@ -8,11 +12,6 @@ import {
Typography, Typography,
} from '@material-ui/core' } from '@material-ui/core'
import Warning from '@material-ui/icons/Warning'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import { Theme, withStyles } from '@material-ui/core/styles'
interface State { interface State {
error?: Error error?: Error
} }
@@ -41,7 +40,7 @@ class ErrorBoundary extends React.Component<Props, State> {
} }
private clearStorage = () => { private clearStorage = () => {
localStorage.clear() PersistantStorage.clear()
window.location = window.location window.location = window.location
} }

View File

@@ -30,8 +30,13 @@ class RemoteStorage implements PersistantStorage {
private expectAck(transactionId: string): Promise<void> { private expectAck(transactionId: string): Promise<void> {
const ack = makeStorageAcknoledgementEvent(transactionId) const ack = makeStorageAcknoledgementEvent(transactionId)
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const callback = () => { const callback = (msg: any) => {
resolve() console.log(msg)
if (msg && msg.error) {
reject(msg.error)
} else {
resolve()
}
rendererEvents.unsubscribe(ack, callback) rendererEvents.unsubscribe(ack, callback)
} }
rendererEvents.subscribe(ack, callback) rendererEvents.subscribe(ack, callback)
@@ -52,8 +57,13 @@ class RemoteStorage implements PersistantStorage {
const promise = new Promise<Model>((resolve, reject) => { const promise = new Promise<Model>((resolve, reject) => {
const callback = (msg: any) => { const callback = (msg: any) => {
const data = msg.data && JSON.parse(msg.data) console.log(msg)
resolve(data)
if (msg.error) {
reject(msg.error)
} else {
resolve(msg.data)
}
rendererEvents.unsubscribe(responseEvent, callback) rendererEvents.unsubscribe(responseEvent, callback)
} }
rendererEvents.subscribe(responseEvent, callback) rendererEvents.subscribe(responseEvent, callback)

View File

@@ -115,7 +115,6 @@ class UpdateNotifier extends React.Component<Props, State> {
vertical: 'top', vertical: 'top',
horizontal: 'right', horizontal: 'right',
} }
console.log(this.state.newerVersions)
return ( return (
<Snackbar <Snackbar
@@ -266,8 +265,8 @@ const styles = (theme: Theme) => ({
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
showUpdateNotification: state.tooBigReducer.showUpdateNotification, showUpdateNotification: state.globalState.showUpdateNotification,
showUpdateDetails: state.tooBigReducer.showUpdateDetails, showUpdateDetails: state.globalState.showUpdateDetails,
} }
} }

View File

@@ -12,6 +12,7 @@ import { Dispatch } from 'redux'
import { MqttOptions } from '../../../backend/src/DataSource' import { MqttOptions } from '../../../backend/src/DataSource'
import { showTree } from './Tree' import { showTree } from './Tree'
import { TopicViewModel } from '../TopicViewModel' import { TopicViewModel } from '../TopicViewModel'
import { showError } from './Global'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => { export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId)) dispatch(connecting(connectionId))
@@ -44,11 +45,6 @@ export const connecting: (connectionId: string) => Action = (connectionId: strin
type: ActionTypes.CONNECTION_SET_CONNECTING, type: ActionTypes.CONNECTION_SET_CONNECTING,
}) })
export const showError = (error?: string) => ({
error,
type: ActionTypes.CONNECTION_SET_SHOW_ERROR,
})
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => { export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId, tree } = getState().connection const { connectionId, tree } = getState().connection
rendererEvents.emit(removeConnection, connectionId) rendererEvents.emit(removeConnection, connectionId)

View File

@@ -1,8 +1,9 @@
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import { ConnectionOptions, createEmptyConnection, makeDefaultConnections } from '../model/ConnectionOptions' import { ConnectionOptions, createEmptyConnection, makeDefaultConnections } from '../model/ConnectionOptions'
import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage' import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { loadLegacyConnectionOptions, clearLegacyConnectionOptions } from '../model/LegacyConnectionSettings' import { showError } from './Global'
import { import {
ActionTypes, ActionTypes,
Action, Action,
@@ -13,8 +14,13 @@ const storedConnectionsIdentifier: StorageIdentifier<{[s: string]: ConnectionOpt
} }
export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => { export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
await ensureConnectionsHaveBeenInitialized() let connections
const connections = await persistantStorage.load(storedConnectionsIdentifier) try {
await ensureConnectionsHaveBeenInitialized()
connections = await persistantStorage.load(storedConnectionsIdentifier)
} catch (error) {
dispatch(showError(error))
}
if (!connections) { if (!connections) {
return return
@@ -27,8 +33,12 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
} }
} }
export const saveConnectionSettings = () => (_dispatch: Dispatch<any>, getState: () => AppState) => { export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
persistantStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections) try {
await persistantStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections)
} catch (error) {
dispatch(showError(error))
}
} }
export const updateConnection = (connectionId: string, changeSet: any): Action => ({ export const updateConnection = (connectionId: string, changeSet: any): Action => ({

View File

@@ -0,0 +1,6 @@
import { ActionTypes } from '../reducers'
export const showError = (error?: string) => ({
error,
type: ActionTypes.showError,
})

View File

@@ -5,5 +5,6 @@ import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection' import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar' import * as sidebarActons from './Sidebar'
import * as connectionManagerActions from './ConnectionManager' import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons, connectionManagerActions } export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons, connectionManagerActions, globalActions }

View File

@@ -1,7 +1,6 @@
import * as React from 'react' import * as React from 'react'
import Delete from '@material-ui/icons/Delete' import Delete from '@material-ui/icons/Delete'
import Settings from '@material-ui/icons/Settings' import Settings from '@material-ui/icons/Settings'
import Notification from './Notification'
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew' import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
import Save from '@material-ui/icons/Save' import Save from '@material-ui/icons/Save'
import Visibility from '@material-ui/icons/Visibility' import Visibility from '@material-ui/icons/Visibility'
@@ -35,7 +34,6 @@ interface Props {
managerActions: typeof connectionManagerActions managerActions: typeof connectionManagerActions
connected: boolean connected: boolean
connecting: boolean connecting: boolean
error?: string
} }
const protocols = [ const protocols = [
@@ -106,19 +104,8 @@ class ConnectionSettings extends React.Component<Props, State> {
</InputAdornment> </InputAdornment>
) )
let renderError = null
if (this.props.error) {
renderError = (
<Notification
message={this.props.error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
return ( return (
<div> <div>
{renderError}
<form className={classes.container} noValidate={true} autoComplete="off"> <form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}> <Grid container={true} spacing={3}>
<Grid item={true} xs={5}> <Grid item={true} xs={5}>
@@ -324,7 +311,6 @@ const mapStateToProps = (state: AppState) => {
return { return {
connected: state.connection.connected, connected: state.connection.connected,
connecting: state.connection.connecting, connecting: state.connection.connecting,
error: state.connection.error,
} }
} }

View File

@@ -147,7 +147,6 @@ class Sidebar extends React.Component<Props, State> {
} }
private valueRenderWidthChange = (width: number) => { private valueRenderWidthChange = (width: number) => {
console.log(width)
this.setState({ valueRenderWidth: width }) this.setState({ valueRenderWidth: width })
} }

View File

@@ -10,16 +10,18 @@ import { ConnectionManagerState, connectionManagerReducer } from './ConnectionMa
export enum ActionTypes { export enum ActionTypes {
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS', showUpdateDetails = 'SHOW_UPDATE_DETAILS',
showError = 'SHOW_ERROR',
} }
export interface CustomAction extends Action { export interface CustomAction extends Action {
type: ActionTypes, type: ActionTypes,
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails?: boolean showUpdateDetails?: boolean
error?: string
} }
export interface AppState { export interface AppState {
tooBigReducer: TooBigOfState globalState: GlobalState
tree: TreeState tree: TreeState
settings: SettingsState, settings: SettingsState,
publish: PublishState publish: PublishState
@@ -27,16 +29,17 @@ export interface AppState {
connectionManager: ConnectionManagerState connectionManager: ConnectionManagerState
} }
export interface TooBigOfState { export interface GlobalState {
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails: boolean showUpdateDetails: boolean
error?: string
} }
const initialBigState: TooBigOfState = { const initialBigState: GlobalState = {
showUpdateDetails: false, showUpdateDetails: false,
} }
const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => { const globalState: Reducer<GlobalState | undefined, CustomAction> = (state = initialBigState, action) => {
if (!state) { if (!state) {
throw Error('No initial state') throw Error('No initial state')
} }
@@ -49,6 +52,12 @@ const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state =
showUpdateNotification: action.showUpdateNotification, showUpdateNotification: action.showUpdateNotification,
} }
case ActionTypes.showError:
return {
...state,
error: action.error,
}
case ActionTypes.showUpdateDetails: case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) { if (action.showUpdateDetails === undefined) {
return state return state
@@ -64,7 +73,7 @@ const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state =
} }
const reducer = combineReducers({ const reducer = combineReducers({
tooBigReducer, globalState,
publish: publishReducer, publish: publishReducer,
connection: connectionReducer, connection: connectionReducer,
settings: settingsReducer, settings: settingsReducer,

View File

@@ -10,32 +10,58 @@ import {
} from '../../events/StorageEvents' } from '../../events/StorageEvents'
export default class ConfigStorage { export default class ConfigStorage {
private adapter: any private file: string
private database: any
constructor(file: string) { constructor(file: string) {
this.adapter = new FileAsync(file) this.file = file
}
private async getDb() {
const adapter = new FileAsync(this.file)
if (!this.database) {
this.database = await lowdb(adapter)
}
return this.database
} }
public async init() { public async init() {
const database: lowdb.LoDashExplicitAsyncWrapper<any> = await lowdb(this.adapter)
backendEvents.subscribe(storageStoreEvent, async (event) => { backendEvents.subscribe(storageStoreEvent, async (event) => {
await database.set(event.store, event.data).write() const ack = makeStorageAcknoledgementEvent(event.transactionId)
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), undefined) try {
const db = await this.getDb()
await db.set(event.store, event.data).write()
backendEvents.emit(ack, undefined)
} catch (error) {
console.error(error)
backendEvents.emit(ack, { error, transactionId: event.transactionId, store: event.store })
}
}) })
backendEvents.subscribe(storageLoadEvent, async (event) => { backendEvents.subscribe(storageLoadEvent, async (event) => {
const responseEvent = makeStorageResponseEvent(event.transactionId) const responseEvent = makeStorageResponseEvent(event.transactionId)
try { try {
const data = await database.get(event.store).value() const db = await this.getDb()
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId }) const data = await db.get(event.store).value()
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId, store: event.store })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
backendEvents.emit(responseEvent, { transactionId: event.transactionId }) backendEvents.emit(responseEvent, { error, transactionId: event.transactionId, store: event.store })
} }
}) })
backendEvents.subscribe(storageClearEvent, async (event) => { backendEvents.subscribe(storageClearEvent, async (event) => {
await database.drop() try {
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), undefined) const db = await this.getDb()
const keys = await db.keys().value()
for (const key of keys) {
await db.unset(key).write()
}
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), undefined)
} catch (error) {
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), { error, transactionId: event.transactionId })
}
}) })
} }
} }

View File

@@ -80,6 +80,3 @@ class UpdateNotifier {
} }
export const updateNotifier = new UpdateNotifier() export const updateNotifier = new UpdateNotifier()
const configStorage = new ConfigStorage('blah.json')
configStorage.init()

View File

@@ -25,7 +25,6 @@ class IpcMainEventBus implements EventBusInterface {
console.log('subscribing', subscribeEvent.topic) console.log('subscribing', subscribeEvent.topic)
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => { this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
this.client = event.sender this.client = event.sender
console.log(subscribeEvent.topic, arg)
callback(arg) callback(arg)
}) })
} }

View File

@@ -5,8 +5,9 @@ interface StorageEvent {
} }
export interface StoreCommand extends StorageEvent { export interface StoreCommand extends StorageEvent {
store: string, store?: string,
data: any data?: any
error?: any
} }
export interface LoadCommand extends StorageEvent { export interface LoadCommand extends StorageEvent {

View File

@@ -1,11 +1,12 @@
import { UpdateInfo } from '../events'
import { BrowserWindow, app, Menu } from 'electron'
import * as path from 'path'
import { menuTemplate } from './MenuTemplate'
import { autoUpdater } from 'electron-updater'
import * as log from 'electron-log' import * as log from 'electron-log'
import * as path from 'path'
import ConfigStorage from '../backend/src/ConfigStorage'
import { app, BrowserWindow, Menu } from 'electron'
import { autoUpdater } from 'electron-updater'
import { ConnectionManager, updateNotifier } from '../backend/src/index' import { ConnectionManager, updateNotifier } from '../backend/src/index'
import { electronTelemetryFactory } from 'electron-telemetry' import { electronTelemetryFactory } from 'electron-telemetry'
import { menuTemplate } from './MenuTemplate'
import { UpdateInfo } from '../events'
const isDev = require('electron-is-dev') const isDev = require('electron-is-dev')
let electronTelemetry: any let electronTelemetry: any
@@ -24,6 +25,9 @@ log.info('App starting...')
const connectionManager = new ConnectionManager() const connectionManager = new ConnectionManager()
connectionManager.manageConnections() connectionManager.manageConnections()
const configStorage = new ConfigStorage(path.join(app.getPath('appData'), app.getName(), 'settings.json'))
configStorage.init()
// Keep a global reference of the window object, if you don't, the window will // 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. // be closed automatically when the JavaScript object is garbage collected.
let mainWindow: BrowserWindow | undefined let mainWindow: BrowserWindow | undefined