Refactor connection

This commit is contained in:
Thomas Nordquist
2019-01-19 14:54:37 +01:00
parent bfcee49e74
commit f3a686b23f
13 changed files with 332 additions and 180 deletions

View File

@@ -30,7 +30,6 @@
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
#-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
}
</style>
<style>

View File

@@ -21,6 +21,7 @@ interface State {
interface Props {
name: string
connectionId: string
theme: Theme
settingsVisible: boolean
}
@@ -93,17 +94,17 @@ class App extends React.Component<Props, State> {
<TitleBar />
<div style={centerContent}>
<div style={this.getStyles().left}>
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
<Tree connectionId={this.props.connectionId} didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node })
}} />
</div>
<div style={this.getStyles().right}>
<Sidebar connectionId={this.state.connectionId} />
<Sidebar connectionId={this.props.connectionId} />
</div>
</div>
</div>
<UpdateNotifier />
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
<Connection />
</ErrorBoundary>
</div >
)
@@ -113,6 +114,7 @@ class App extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.settings.visible,
connectionId: state.connectionId,
}
}

View File

@@ -0,0 +1,40 @@
import { ActionTypes, NodeOrder, CustomAction, AppState } from '../reducers'
import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (dataSourceState) => {
if (dataSourceState.connected) {
dispatch(connected())
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
})
}
export const connected: () => CustomAction = () => ({
type: ActionTypes.connected,
})
export const connecting: (connectionId: string) => CustomAction = (connectionId: string) => ({
connectionId,
type: ActionTypes.connecting,
})
export const showError = (error?: string) => ({
error,
type: ActionTypes.showError,
})
export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => {
rendererEvents.emit(removeConnection, getState().connectionId)
dispatch({
type: ActionTypes.disconnect,
})
}

View File

@@ -2,5 +2,6 @@ import * as settingsActions from './Settings'
import * as sidebarActions from './Sidebar'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
export { settingsActions, treeActions, sidebarActions, updateNotifierActions }
export { settingsActions, treeActions, sidebarActions, updateNotifierActions, connectionActions }

3
app/src/bugtracking.ts Normal file
View File

@@ -0,0 +1,3 @@
import { electronRendererTelementry } from 'electron-telemetry'
const spareMeFromGc = electronRendererTelementry

View File

@@ -18,21 +18,27 @@ import {
Toolbar,
Typography,
} from '@material-ui/core'
import { DataSourceState, MqttOptions } from '../../../../backend/src/DataSource'
import { connect } from 'react-redux'
import { MqttOptions } from '../../../../backend/src/DataSource'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../../events'
import Notification from './Notification'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
import sha1 = require('sha1')
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connectionActions } from '../../actions'
interface Props {
classes: {[s: string]: string}
theme: Theme
onAbort: () => void,
onConnection: (connectionId: string) => void
actions: typeof connectionActions,
visible: boolean
connected: boolean
connecting: boolean
error?: string
}
const protocols = [
@@ -41,57 +47,64 @@ const protocols = [
]
interface State {
connecting: boolean
connectionId?: string
error?: string
visible: boolean
showPassword: boolean
connectionSettings: ConnectionSettings
}
interface ConnectionSettings {
host: string
protocol: string
port: number
tls: boolean
certValidation: boolean
clientId: string
connectionId?: string
username: string
password: string
showPassword: boolean
}
declare var window: any
class Connection extends React.Component<Props, State> {
private randomClientId: tring
private randomClientId: string
private defaultConnectionSettings: ConnectionSettings = {
host: 'iot.eclipse.org',
protocol: protocols[0],
port: 1883,
tls: false,
certValidation: true,
clientId: '',
username: '',
password: '',
connectionId: undefined,
}
constructor(props: any) {
super(props)
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
this.randomClientId = `mqtt-explorer-${clientIdSha}`
this.state = {
connectionSettings: this.loadConnectionSettings(),
showPassword: false,
}
}
private loadConnectionSettings(): ConnectionSettings {
let storedSettings: ConnectionSettings | undefined
const storedSettingsString = window.localStorage.getItem('connectionSettings')
let storedSettings
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
this.randomClientId = `mqtt-explorer-${clientIdSha}`
const defaultState = {
visible: true,
host: 'iot.eclipse.org',
protocol: protocols[0],
port: 1883,
tls: false,
certValidation: true,
clientId: '',
username: '',
password: '',
connecting: false,
connectionId: undefined,
showPassword: false,
}
this.state = Object.assign({}, defaultState, storedSettings)
return storedSettings || this.defaultConnectionSettings
}
private saveConnectionSettings() {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state))
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state.connectionSettings))
}
private handleClickShowPassword = () => {
@@ -99,49 +112,19 @@ class Connection extends React.Component<Props, State> {
}
private optionsFromState(): MqttOptions {
const protocol = this.state.protocol === 'tcp://' ? 'mqtt://' : this.state.protocol
const url = `${protocol}${this.state.host}:${this.state.port}`
const protocol = this.state.connectionSettings.protocol === 'tcp://' ? 'mqtt://' : this.state.connectionSettings.protocol
const url = `${protocol}${this.state.connectionSettings.host}:${this.state.connectionSettings.port}`
return {
url,
username: this.state.username || undefined,
password: this.state.password || undefined,
clientId: this.state.clientId || this.randomClientId,
tls: this.state.tls,
certValidation: this.state.certValidation,
username: this.state.connectionSettings.username || undefined,
password: this.state.connectionSettings.password || undefined,
clientId: this.state.connectionSettings.clientId || this.randomClientId,
tls: this.state.connectionSettings.tls,
certValidation: this.state.connectionSettings.certValidation,
}
}
private connect() {
this.setState({
connecting: true,
})
const options = this.optionsFromState()
const connectionId = (sha1(Math.random() + JSON.stringify(options)).slice(0, 8)) as string
this.setState({ connectionId })
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (state: DataSourceState) => {
console.log(state)
if (state.connected) {
this.props.onConnection(connectionId)
this.setState({ visible: false })
} else if (state.error) {
this.setState({ error: state.error })
this.disconnect()
}
})
}
private disconnect() {
this.setState({
connecting: false,
})
rendererEvents.emit(removeConnection, this.state.connectionId)
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
root: {
@@ -176,10 +159,12 @@ class Connection extends React.Component<Props, State> {
}
private handleChange = (name: string) => (event: any) => {
const state: any = {
[name]: event.target.value,
}
this.setState(state)
this.setState({
connectionSettings: {
...this.state.connectionSettings,
[name]: event.target.value,
},
})
}
public render() {
@@ -197,12 +182,11 @@ class Connection extends React.Component<Props, State> {
)
let renderError = null
if (this.state.error) {
if (this.props.error) {
renderError = (
<Notification
message={this.state.error}
type="error"
onClose={() => { this.setState({ error: undefined }) }}
message={this.props.error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
@@ -210,7 +194,7 @@ class Connection extends React.Component<Props, State> {
return (
<div>
{renderError}
<Modal open={this.state.visible} disableAutoFocus={true}>
<Modal open={this.props.visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
@@ -218,26 +202,13 @@ class Connection extends React.Component<Props, State> {
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={24}>
<Grid item={true} xs={2}>
<TextField
select={true}
label="Protocol"
className={classes.textField}
value={this.state.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))}
</TextField>
{this.renderProtocols()}
</Grid>
<Grid item xs={7}>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={this.state.host}
value={this.state.connectionSettings.host}
onChange={this.handleChange('host')}
margin="normal"
/>
@@ -246,7 +217,7 @@ class Connection extends React.Component<Props, State> {
<TextField
label="Port"
className={classes.textField}
value={this.state.port}
value={this.state.connectionSettings.port}
onChange={this.handleChange('port')}
margin="normal"
/>
@@ -255,7 +226,7 @@ class Connection extends React.Component<Props, State> {
<TextField
label="Username"
className={classes.textField}
value={this.state.username}
value={this.state.connectionSettings.username}
onChange={this.handleChange('username')}
margin="normal"
/>
@@ -266,7 +237,7 @@ class Connection extends React.Component<Props, State> {
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={this.state.password}
value={this.state.connectionSettings.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
@@ -278,41 +249,17 @@ class Connection extends React.Component<Props, State> {
<Input
placeholder={this.randomClientId}
className={classes.textField}
value={this.state.clientId || ''}
value={this.state.connectionSettings.clientId || ''}
onChange={this.handleChange('clientId')}
startAdornment={<span />}
/>
</FormControl>
</Grid>
<Grid item={true} xs={4}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.certValidation}
onChange={() => this.setState({ certValidation: !this.state.certValidation })}
color="primary"
/>
)}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.tls}
onChange={() => this.setState({ tls: !this.state.tls })}
color="primary"
/>
)}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
{this.renderTlsSwitch()}
</Grid>
</Grid>
<br />
@@ -329,12 +276,90 @@ class Connection extends React.Component<Props, State> {
)
}
private renderConnectButton() {
private renderProtocols() {
const { classes } = this.props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))
if (this.state.connecting) {
return (
<TextField
select={true}
label="Protocol"
className={classes.textField}
value={this.state.connectionSettings.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocolItems}
</TextField>
)
}
private renderCertValidationSwitch() {
const { classes } = this.props
const certSwitch = (
<Switch
checked={this.state.connectionSettings.certValidation}
onChange={this.toggleCertValidation}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={certSwitch}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
)
}
private toggleCertValidation = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
certValidation: !this.state.connectionSettings.certValidation,
},
})
private renderTlsSwitch() {
const { classes } = this.props
const tlsSwitch = (
<Switch
checked={this.state.connectionSettings.tls}
onChange={this.toggleTls}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={tlsSwitch}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
)
}
private toggleTls = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
tls: !this.state.connectionSettings.tls,
},
})
private renderConnectButton() {
const { classes, actions } = this.props
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickAbort}>
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort
</Button>
)
@@ -347,12 +372,25 @@ class Connection extends React.Component<Props, State> {
}
private onClickConnect = () => {
this.connect()
}
private onClickAbort = () => {
this.disconnect()
const connectionId = String(sha1(String(Math.random())).slice(0, 8))
const options = this.optionsFromState()
this.props.actions.connect(options, connectionId)
}
}
export default withStyles(Connection.styles, { withTheme: true })(Connection)
const mapStateToProps = (state: AppState) => {
return {
visible: !state.connected,
connected: state.connected,
connecting: state.connecting,
error: state.error,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(Connection.styles, { withTheme: true })(Connection))

View File

@@ -4,14 +4,8 @@ import { Snackbar, SnackbarContent } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { green, red } from '@material-ui/core/colors'
enum MessageType {
success = 'success',
error = 'error',
}
interface Props {
message?: string
type: MessageType
onClose: () => void
classes: any
}
@@ -33,18 +27,20 @@ class Notification extends React.Component<Props, {}> {
})
public render() {
const snackbarAnchor = {
vertical: 'bottom' as 'bottom',
horizontal: 'left' as 'left',
}
return (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
anchorOrigin={snackbarAnchor}
open={Boolean(this.props.message)}
autoHideDuration={10000}
onClose={this.props.onClose}
>
<SnackbarContent
className={this.props.classes[this.props.type]}
className={this.props.classes.error}
message={this.props.message}
/>
</Snackbar>

View File

@@ -1,15 +1,16 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import { AppBar, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import CloudOff from '@material-ui/icons/CloudOff'
import Menu from '@material-ui/icons/Menu'
import Search from '@material-ui/icons/Search'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { settingsActions } from '../actions'
import { settingsActions, connectionActions } from '../actions'
const styles: StyleRulesCallback = theme => ({
title: {
@@ -85,7 +86,7 @@ class TitleBar extends React.Component<Props, State> {
}
public render() {
const { classes } = this.props
const { actions, classes } = this.props
return (
<AppBar position="static">
@@ -94,6 +95,9 @@ class TitleBar extends React.Component<Props, State> {
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
</Toolbar>
</AppBar>
)
@@ -119,8 +123,8 @@ class TitleBar extends React.Component<Props, State> {
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(settingsActions, dispatch),
actions: { ...bindActionCreators(connectionActions, dispatch), ...bindActionCreators(settingsActions, dispatch) },
}
}
export default withStyles(styles)(connect(null, mapDispatchToProps)(TitleBar))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(TitleBar))

View File

@@ -18,6 +18,7 @@ interface Props {
autoExpandLimit: number
didSelectNode?: (node: q.TreeNode) => void
connectionId?: string
connected: boolean
}
interface TreeState {
@@ -32,8 +33,7 @@ class Tree extends React.Component<Props, TreeState> {
constructor(props: any) {
super(props)
const tree = new q.Tree()
this.state = { tree, msg: {} }
this.state = { tree: new q.Tree(), msg: {} }
}
public time(): number {
@@ -67,20 +67,18 @@ class Tree extends React.Component<Props, TreeState> {
}
public componentWillReceiveProps(nextProps: Props) {
if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.unsubscribeAll(event)
}
if (nextProps.connectionId) {
const event = makeConnectionMessageEvent(nextProps.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
}
this.registerAndUnregisterEventSubscriptionsForNewProps(nextProps)
}
public componentDidMount() {
if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
private registerAndUnregisterEventSubscriptionsForNewProps(nextProps: Props) {
if (this.props.connectionId !== nextProps.connectionId) {
if (this.props.connectionId) {
this.setState({ tree: new q.Tree() })
rendererEvents.unsubscribeAll(makeConnectionMessageEvent(this.props.connectionId))
}
if (nextProps.connectionId) {
rendererEvents.subscribe(makeConnectionMessageEvent(nextProps.connectionId), this.handleNewData)
}
}
}
@@ -92,6 +90,7 @@ class Tree extends React.Component<Props, TreeState> {
}
private handleNewData = (msg: any) => {
console.log('new data')
const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString())
this.state.tree.updateWithNode(node.firstNode())
@@ -100,7 +99,9 @@ class Tree extends React.Component<Props, TreeState> {
}
public render() {
console.log('render called')
if (!this.props.connected) {
return null
}
const style: React.CSSProperties = {
lineHeight: '1.1',
@@ -129,6 +130,7 @@ class Tree extends React.Component<Props, TreeState> {
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
connected: state.connected,
}
}

View File

@@ -2,25 +2,24 @@ import './tracking'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import reduxThunk from 'redux-thunk'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers, { AppState, NodeOrder } from './reducers'
import App from './App'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { createStore, applyMiddleware, compose } from 'redux'
const initialAppState: AppState = {
settings: {
autoExpandLimit: 0,
nodeOrder: NodeOrder.none,
visible: false,
},
sidebar: {},
selectedTopic: undefined,
showUpdateDetails: false,
}
const store = createStore(reducers, initialAppState)
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
reducers,
composeEnhancers(
applyMiddleware(
reduxThunk,
),
),
)
const theme = createMuiTheme({
palette: {

View File

@@ -3,8 +3,12 @@ import * as q from '../../../backend/src/Model'
import { Action, Reducer } from 'redux'
import { trackEvent } from '../tracking'
import { MqttOptions, DataSourceStateMachine } from '../../../backend/src/DataSource'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent } from '../../../events'
export enum ActionTypes {
disconnect = 'DISCONNECT',
showError = 'SHOW_ERROR',
setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT',
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
setNodeOrder = 'SET_NODE_ORDER',
@@ -13,6 +17,8 @@ export enum ActionTypes {
setPublishPayload = 'SET_PUBLISH_PAYLOAD',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
connecting = 'CONNECTING',
connected = 'CONNECTED',
}
export interface CustomAction extends Action {
@@ -24,6 +30,9 @@ export interface CustomAction extends Action {
publishPayload?: string
showUpdateNotification?: boolean
showUpdateDetails?: boolean
connectionOptions?: MqttOptions
connectionId?: string
error?: string
}
export interface SidebarState {
@@ -37,6 +46,10 @@ export interface AppState {
sidebar: SidebarState
showUpdateNotification?: boolean
showUpdateDetails: boolean
connecting: boolean
connected: boolean
error?: string
connectionId?: string
}
export interface SettingsState {
@@ -52,7 +65,21 @@ export enum NodeOrder {
topics = '#topics',
}
const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) => {
const initialAppState: AppState = {
settings: {
autoExpandLimit: 0,
nodeOrder: NodeOrder.none,
visible: false,
},
sidebar: {},
selectedTopic: undefined,
showUpdateDetails: false,
connected: false,
connecting: false,
error: undefined,
}
const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialAppState, action) => {
if (!state) {
throw Error('No initial state')
}
@@ -117,6 +144,40 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
...state,
showUpdateDetails: action.showUpdateDetails,
}
case ActionTypes.connecting:
if (!action.connectionId) {
return state
}
return {
...state,
connected: false,
connecting: true,
connectionId: action.connectionId,
}
case ActionTypes.disconnect:
return {
...state,
connected: false,
connecting: false,
connectionId: undefined,
}
case ActionTypes.connected:
return {
...state,
connected: true,
connecting: false,
}
case ActionTypes.showError:
return {
...state,
error: action.error,
}
default:
return state
}

6
package-lock.json generated
View File

@@ -4227,6 +4227,12 @@
"strip-indent": "^1.0.1"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==",
"dev": true
},
"reflect-metadata": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",

View File

@@ -55,6 +55,7 @@
"mocha": "^5.2.0",
"mustache": "^3.0.1",
"nyc": "^13.1.0",
"redux-thunk": "^2.3.0",
"source-map-support": "^0.5.9",
"ts-node": "^7.0.1",
"tslint": "^5.12.0",