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 CssBaseline from '@material-ui/core/CssBaseline'
import ErrorBoundary from './ErrorBoundary'
import Notification from './components/Notification'
import Sidebar from './components/Sidebar/Sidebar'
import TitleBar from './components/TitleBar'
import Tree from './components/Tree/Tree'
import UpdateNotifier from './UpdateNotifier'
import { AppState } from './reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { default as SplitPane } from 'react-split-pane'
import { globalActions } from './actions'
import { Theme, withStyles } from '@material-ui/core/styles'
const Settings = React.lazy(() => import('./components/Settings'))
@@ -18,6 +21,8 @@ interface Props {
connectionId: string
classes: any
settingsVisible: boolean
error?: string
actions: any
}
class App extends React.PureComponent<Props, {}> {
@@ -26,6 +31,18 @@ class App extends React.PureComponent<Props, {}> {
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() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes
@@ -34,6 +51,7 @@ class App extends React.PureComponent<Props, {}> {
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
{this.renderError()}
<React.Suspense fallback={<div>Loading...</div>}>
<Settings />
</React.Suspense>
@@ -75,6 +93,7 @@ const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.settings.visible,
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 PersistantStorage from './PersistantStorage'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning'
import { electronRendererTelementry } from 'electron-telemetry'
import { Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
Modal,
@@ -8,11 +12,6 @@ import {
Typography,
} 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 {
error?: Error
}
@@ -41,7 +40,7 @@ class ErrorBoundary extends React.Component<Props, State> {
}
private clearStorage = () => {
localStorage.clear()
PersistantStorage.clear()
window.location = window.location
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import { ConnectionOptions, createEmptyConnection, makeDefaultConnections } from '../model/ConnectionOptions'
import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Dispatch } from 'redux'
import { loadLegacyConnectionOptions, clearLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import { showError } from './Global'
import {
ActionTypes,
Action,
@@ -13,8 +14,13 @@ const storedConnectionsIdentifier: StorageIdentifier<{[s: string]: ConnectionOpt
}
export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
let connections
try {
await ensureConnectionsHaveBeenInitialized()
const connections = await persistantStorage.load(storedConnectionsIdentifier)
connections = await persistantStorage.load(storedConnectionsIdentifier)
} catch (error) {
dispatch(showError(error))
}
if (!connections) {
return
@@ -27,8 +33,12 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
}
}
export const saveConnectionSettings = () => (_dispatch: Dispatch<any>, getState: () => AppState) => {
persistantStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections)
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
await persistantStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections)
} catch (error) {
dispatch(showError(error))
}
}
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 sidebarActons from './Sidebar'
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 Delete from '@material-ui/icons/Delete'
import Settings from '@material-ui/icons/Settings'
import Notification from './Notification'
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
import Save from '@material-ui/icons/Save'
import Visibility from '@material-ui/icons/Visibility'
@@ -35,7 +34,6 @@ interface Props {
managerActions: typeof connectionManagerActions
connected: boolean
connecting: boolean
error?: string
}
const protocols = [
@@ -106,19 +104,8 @@ class ConnectionSettings extends React.Component<Props, State> {
</InputAdornment>
)
let renderError = null
if (this.props.error) {
renderError = (
<Notification
message={this.props.error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
return (
<div>
{renderError}
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={5}>
@@ -324,7 +311,6 @@ const mapStateToProps = (state: AppState) => {
return {
connected: state.connection.connected,
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) => {
console.log(width)
this.setState({ valueRenderWidth: width })
}

View File

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

View File

@@ -10,32 +10,58 @@ import {
} from '../../events/StorageEvents'
export default class ConfigStorage {
private adapter: any
private file: string
private database: any
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() {
const database: lowdb.LoDashExplicitAsyncWrapper<any> = await lowdb(this.adapter)
backendEvents.subscribe(storageStoreEvent, async (event) => {
await database.set(event.store, event.data).write()
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), undefined)
const ack = makeStorageAcknoledgementEvent(event.transactionId)
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) => {
const responseEvent = makeStorageResponseEvent(event.transactionId)
try {
const data = await database.get(event.store).value()
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId })
const db = await this.getDb()
const data = await db.get(event.store).value()
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId, store: event.store })
} catch (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) => {
await database.drop()
try {
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()
const configStorage = new ConfigStorage('blah.json')
configStorage.init()

View File

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

View File

@@ -5,8 +5,9 @@ interface StorageEvent {
}
export interface StoreCommand extends StorageEvent {
store: string,
data: any
store?: string,
data?: any
error?: any
}
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 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 { electronTelemetryFactory } from 'electron-telemetry'
import { menuTemplate } from './MenuTemplate'
import { UpdateInfo } from '../events'
const isDev = require('electron-is-dev')
let electronTelemetry: any
@@ -24,6 +25,9 @@ log.info('App starting...')
const connectionManager = new ConnectionManager()
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
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow: BrowserWindow | undefined