This commit is contained in:
Thomas Nordquist
2019-04-04 19:51:44 +02:00
parent c20c075bcf
commit 09dcce97b7
55 changed files with 775 additions and 1415 deletions

View File

@@ -33,10 +33,6 @@ class App extends React.PureComponent<Props, {}> {
this.state = { }
}
public componentDidMount() {
this.props.settingsActions.loadSettings()
}
private renderError() {
if (this.props.error) {
const error = typeof this.props.error === 'string' ? this.props.error : JSON.stringify(this.props.error)
@@ -49,6 +45,10 @@ class App extends React.PureComponent<Props, {}> {
}
}
public componentDidMount() {
this.props.settingsActions.loadSettings()
}
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes

View File

@@ -21,19 +21,14 @@ interface Props {
}
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
}
public componentDidCatch(error: Error, errorInfo: any) {
electronRendererTelementry.trackError(error)
console.log('did catch', error)
}
public static getDerivedStateFromError(error: Error) {
return { error }
}
constructor(props: Props) {
super(props)
this.state = {}
}
private restart = () => {
window.location = window.location
@@ -44,6 +39,11 @@ class ErrorBoundary extends React.Component<Props, State> {
window.location = window.location
}
public componentDidCatch(error: Error, errorInfo: any) {
electronRendererTelementry.trackError(error)
console.log('did catch', error)
}
public render() {
if (!this.state.error) {
return this.props.children

View File

@@ -102,15 +102,6 @@ class UpdateNotifier extends React.Component<Props, State> {
this.props.actions.showUpdateDetails(false)
}
public render() {
return (
<div>
{this.renderUpdateNotification()}
{this.renderUpdateDetails()}
</div>
)
}
private renderUpdateNotification() {
const snackbarAnchor: any = {
vertical: 'top',
@@ -207,7 +198,7 @@ class UpdateNotifier extends React.Component<Props, State> {
private renderDownloads() {
const latestUpdate = this.state.newerVersions[0]
if (!latestUpdate || !latestUpdate.assets) {
if (!latestUpdate || !latestUpdate.assets) {
return null
}
@@ -224,6 +215,15 @@ class UpdateNotifier extends React.Component<Props, State> {
</div>
))
}
public render() {
return (
<div>
{this.renderUpdateNotification()}
{this.renderUpdateDetails()}
</div>
)
}
}
const styles = (theme: Theme) => ({

View File

@@ -70,7 +70,7 @@ async function openCertificate(): Promise<CertificateParameters> {
return
}
if (data.length > 16_384 || data.length < 128) {
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}

View File

@@ -4,7 +4,7 @@ import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events'
export const setTopic = (topic?: string): Action => {
export const setTopic = (topic?: string): Action => {
return {
topic,
type: ActionTypes.PUBLISH_SET_TOPIC,

View File

@@ -62,7 +62,7 @@ export const selectTopicWithMouseOver = (selectTopicWithMouseOver: boolean) => (
dispatch(storeSettings())
}
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>, getState: () => AppState) => {
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
@@ -100,7 +100,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
type: ActionTypes.SETTINGS_FILTER_TOPICS,
})
if (!filterStr || !tree) {
if (!filterStr || !tree) {
dispatch(batchActions([setAutoExpandLimit(0), (showTree(tree) as any)]))
return
}

View File

@@ -19,7 +19,7 @@ const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispat
// Update publish topic
let setTopicDispatch: any | undefined
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
setTopicDispatch = setTopic(topic.path())
}

View File

@@ -40,6 +40,15 @@ class BrokerStatistics extends React.Component<Props, {}> {
this.state = {}
}
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div className={this.props.classes.flex}>
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{this.renderStat(tree, b)}</div>
</div>
)
}
public render() {
const { tree, classes } = this.props
if (!tree || !tree.findNode('$SYS/broker/clients/total')) {
@@ -96,15 +105,6 @@ class BrokerStatistics extends React.Component<Props, {}> {
)
}
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div className={this.props.classes.flex}>
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{this.renderStat(tree, b)}</div>
</div>
)
}
public renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node || !node.message) {

View File

@@ -19,44 +19,9 @@ class CodeDiff extends React.Component<Props, {}> {
super(props)
}
public render() {
const changes = diff.diffLines(this.props.previous, this.props.current)
const styledLines = Prism.highlight(this.props.current, Prism.languages.json).split('\n')
let lineNumber = 0
const code = changes.map((change, key) => {
const hasStyledCode = change.removed !== true
const changedLines = change.count || 0
if (hasStyledCode && this.props.language === 'json') {
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
const lines = currentLines.map((l, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)} dangerouslySetInnerHTML={{ __html: l }} /></div>
})
lineNumber += changedLines
return <div key={key}>{lines}</div>
}
return this.trimNewlineRight(change.value)
.split('\n')
.map((line, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)}>{line}</span></div>
})
})
return (
<div style={{ backgroundColor: '#ebebeb' }}>
<pre className={`language-json ${this.props.classes.codeBlock}`}>
{code}
</pre>
{this.renderChangeAmount(changes)}
</div>
)
}
private renderChangeAmount(changes: Diff.Change[]) {
const additions = changes.map(change => (change.added === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
const deletions = changes.map(change => (change.removed === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
const additions = changes.map(change => (change.added === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
const deletions = changes.map(change => (change.removed === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
if (additions === 0 && deletions === 0) {
return null
}
@@ -92,6 +57,41 @@ class CodeDiff extends React.Component<Props, {}> {
return this.props.classes.noChange
}
public render() {
const changes = diff.diffLines(this.props.previous, this.props.current)
const styledLines = Prism.highlight(this.props.current, Prism.languages.json).split('\n')
let lineNumber = 0
const code = changes.map((change, key) => {
const hasStyledCode = change.removed !== true
const changedLines = change.count || 0
if (hasStyledCode && this.props.language === 'json') {
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
const lines = currentLines.map((l, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)} dangerouslySetInnerHTML={{ __html: l }} /></div>
})
lineNumber += changedLines
return <div key={key}>{lines}</div>
}
return this.trimNewlineRight(change.value)
.split('\n')
.map((line, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)}>{line}</span></div>
})
})
return (
<div style={{ backgroundColor: '#ebebeb' }}>
<pre className={`language-json ${this.props.classes.codeBlock}`}>
{code}
</pre>
{this.renderChangeAmount(changes)}
</div>
)
}
}
const style = (theme: Theme) => {

View File

@@ -41,7 +41,7 @@ class ConnectionHealthIndicator extends React.Component<Props, {}> {
public render() {
const { classes, health, connected } = this.props
if (!health || !connected) {
if (!health || !connected) {
return null
}
@@ -56,7 +56,7 @@ class ConnectionHealthIndicator extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => {
return {
health: state.connection.health,
connected: state.connection.connected || state.connection.connecting,
connected: state.connection.connected || state.connection.connecting,
}
}

View File

@@ -44,6 +44,40 @@ class ConnectionSettings extends React.Component<Props, State> {
})
}
private renderCertificateInfo() {
if (!this.props.connection.selfSignedCertificate) {
return null
}
return (
<span>
<Tooltip title={this.props.connection.selfSignedCertificate.name}>
<Typography className={this.props.classes.certificateName}>
<ClearAdornment action={this.clearCertificate} value={this.props.connection.selfSignedCertificate.name} />
{this.props.connection.selfSignedCertificate.name}
</Typography>
</Tooltip>
</span>
)
}
private clearCertificate = () => {
this.props.managerActions.updateConnection(this.props.connection.id, {
selfSignedCertificate: undefined,
})
}
private renderSubscriptions() {
const connection = this.props.connection
return connection.subscriptions.map(subscription => (
<Subscription
deleteAction={() => this.props.managerActions.deleteSubscription(subscription, connection.id)}
subscription={subscription}
key={subscription}
/>
))
}
public render() {
const { classes } = this.props
return (
@@ -115,40 +149,6 @@ class ConnectionSettings extends React.Component<Props, State> {
</div>
)
}
private renderCertificateInfo() {
if (!this.props.connection.selfSignedCertificate) {
return null
}
return (
<span>
<Tooltip title={this.props.connection.selfSignedCertificate.name}>
<Typography className={this.props.classes.certificateName}>
<ClearAdornment action={this.clearCertificate} value={this.props.connection.selfSignedCertificate.name} />
{this.props.connection.selfSignedCertificate.name}
</Typography>
</Tooltip>
</span>
)
}
private clearCertificate = () => {
this.props.managerActions.updateConnection(this.props.connection.id, {
selfSignedCertificate: undefined,
})
}
private renderSubscriptions() {
const connection = this.props.connection
return connection.subscriptions.map(subscription => (
<Subscription
deleteAction={() => this.props.managerActions.deleteSubscription(subscription, connection.id)}
subscription={subscription}
key={subscription}
/>
))
}
}
const Subscription = (props: {

View File

@@ -90,105 +90,6 @@ class ConnectionSettings extends React.Component<Props, State> {
})
}
public render() {
const { classes, connection } = this.props
const passwordVisibilityButton = (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
>
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={5}>
<TextField
label="Name"
className={classes.textField}
value={connection.name}
onChange={this.handleChange('name')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={4}>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
{this.renderTlsSwitch()}
</Grid>
<Grid item={true} xs={2}>
{this.renderProtocols()}
</Grid>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={connection.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={connection.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
{this.requiresBasePath() ? this.renderBasePathInput() : null}
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
<TextField
label="Username"
className={classes.textField}
value={connection.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={connection.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
</FormControl>
</Grid>
</Grid>
<br />
<div>
<div style={{ float: 'left' }}>
<Button variant="contained" className={classes.button} onClick={() => this.props.managerActions.deleteConnection(this.props.connection.id)}>
Delete <Delete />
</Button>
<Button variant="contained" className={classes.button} onClick={this.props.managerActions.toggleAdvancedSettings}>
<Settings /> Advanced
</Button>
</div>
<div style={{ float : 'right' }}>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.managerActions.saveConnectionSettings}>
<Save /> Save
</Button>
{this.renderConnectButton()}
</div>
</div>
</form>
</div>
)
}
private renderProtocols() {
const { classes, connection } = this.props
@@ -305,6 +206,105 @@ class ConnectionSettings extends React.Component<Props, State> {
this.props.actions.connect(mqttOptions, this.props.connection.id)
}
}
public render() {
const { classes, connection } = this.props
const passwordVisibilityButton = (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
>
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={5}>
<TextField
label="Name"
className={classes.textField}
value={connection.name}
onChange={this.handleChange('name')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={4}>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
{this.renderTlsSwitch()}
</Grid>
<Grid item={true} xs={2}>
{this.renderProtocols()}
</Grid>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={connection.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={connection.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
{this.requiresBasePath() ? this.renderBasePathInput() : null}
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
<TextField
label="Username"
className={classes.textField}
value={connection.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={connection.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
</FormControl>
</Grid>
</Grid>
<br />
<div>
<div style={{ float: 'left' }}>
<Button variant="contained" className={classes.button} onClick={() => this.props.managerActions.deleteConnection(this.props.connection.id)}>
Delete <Delete />
</Button>
<Button variant="contained" className={classes.button} onClick={this.props.managerActions.toggleAdvancedSettings}>
<Settings /> Advanced
</Button>
</div>
<div style={{ float : 'right' }}>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.managerActions.saveConnectionSettings}>
<Save /> Save
</Button>
{this.renderConnectButton()}
</div>
</div>
</form>
</div>
)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -29,6 +29,20 @@ class ConnectionSetup extends React.Component<Props, {}> {
super(props)
}
private renderSettings() {
const { connection, showAdvancedSettings } = this.props
if (!connection) {
return null
}
return (
<div>
<Collapse in={!showAdvancedSettings}><ConnectionSettings connection={connection} /></Collapse>
<Collapse in={showAdvancedSettings}><AdvancedConnectionSettings connection={connection} /></Collapse>
</div>
)
}
public componentDidMount() {
this.props.actions.loadConnectionSettings()
}
@@ -55,20 +69,6 @@ class ConnectionSetup extends React.Component<Props, {}> {
</div>
)
}
private renderSettings() {
const { connection, showAdvancedSettings } = this.props
if (!connection) {
return null
}
return (
<div>
<Collapse in={!showAdvancedSettings}><ConnectionSettings connection={connection} /></Collapse>
<Collapse in={showAdvancedSettings}><AdvancedConnectionSettings connection={connection} /></Collapse>
</div>
)
}
}
const connectionHeight = '440px'

View File

@@ -92,7 +92,7 @@ const connectionItemRenderer = withStyles(connectionItemStyle)((props: Connectio
onClick={() => props.actions.selectConnection(props.connection.id)}
>
<Typography className={props.classes.name}>
{props.connection.name || 'mqtt broker'}
{props.connection.name || 'mqtt broker'}
</Typography>
<Typography className={props.classes.details}>
{connection && connection.url}

View File

@@ -31,6 +31,16 @@ class Copy extends React.Component<Props, State> {
this.state = { didCopy: false, snackBarOpen: false }
}
private handleClick = (event: React.MouseEvent) => {
event.stopPropagation()
copy(this.props.value)
this.setState({ didCopy: true, snackBarOpen: true })
setTimeout(() => {
this.setState({ didCopy: false })
}, 1500)
}
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" />
@@ -64,16 +74,6 @@ class Copy extends React.Component<Props, State> {
</span>
)
}
private handleClick = (event: React.MouseEvent) => {
event.stopPropagation()
copy(this.props.value)
this.setState({ didCopy: true, snackBarOpen: true })
setTimeout(() => {
this.setState({ didCopy: false })
}, 1500)
}
}
export default withStyles(styles)(Copy)

View File

@@ -19,16 +19,16 @@ class CustomIconButton extends React.Component<Props, {}> {
super(props)
}
private onClick = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.onClick(event)
}
public render() {
return (
<IconButton className={this.props.classes.button} onClick={this.onClick}>{this.props.children}</IconButton>
)
}
private onClick = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.onClick(event)
}
}
export default withStyles(styles)(CustomIconButton)

View File

@@ -18,18 +18,6 @@ class Demo extends React.Component<{}, State> {
this.state = { enabled: false, target: { x: 0, y: 0 }, position: { x: 0, y: 0 }, stepSizeX: 1, stepSizeY: 1 }
}
public componentDidMount() {
(window as any).demo.enableMouse = () => {
this.setState({ enabled: true })
}
(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
this.moveCloser()
}
}
private moveCloser(steps: number = 0) {
const steSizeX = Math.min(this.state.stepSizeX, Math.abs(this.state.position.x - this.state.target.x))
const steSizeY = Math.min(this.state.stepSizeY, Math.abs(this.state.position.y - this.state.target.y))
@@ -53,6 +41,18 @@ class Demo extends React.Component<{}, State> {
}, this.frameInterval)
}
public componentDidMount() {
(window as any).demo.enableMouse = () => {
this.setState({ enabled: true })
}
(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
this.moveCloser()
}
}
public render() {
if (!this.state.enabled) {
return null

View File

@@ -28,6 +28,35 @@ class PauseButton extends React.Component<Props, {changes: number}> {
this.state = { changes: 0 }
}
private renderResume() {
return (
<Tooltip title="Resumes updating the tree, after applying all recorded changes">
<Resume />
</Tooltip>
)
}
private renderPause() {
return (
<Tooltip title="Stops all updates, records changes until the buffer is full.">
<Pause />
</Tooltip>
)
}
private renderBufferStats() {
if (!this.props.tree) {
return
}
return (
<span>
{this.state.changes} changes<br />
buffer at {Math.round(this.props.tree.unmergedChanges().fillState() * 10000) / 100}%
</span>
)
}
public componentDidMount() {
this.timer = setInterval(() => {
if (!this.props.paused || !this.props.tree) {
@@ -59,35 +88,6 @@ class PauseButton extends React.Component<Props, {changes: number}> {
</div>
)
}
private renderResume() {
return (
<Tooltip title="Resumes updating the tree, after applying all recorded changes">
<Resume />
</Tooltip>
)
}
private renderPause() {
return (
<Tooltip title="Stops all updates, records changes until the buffer is full.">
<Pause />
</Tooltip>
)
}
private renderBufferStats() {
if (!this.props.tree) {
return
}
return (
<span>
{this.state.changes} changes<br />
buffer at {Math.round(this.props.tree.unmergedChanges().fillState() * 10000) / 100}%
</span>
)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -86,45 +86,6 @@ class Settings extends React.Component<Props, {}> {
this.state = {}
}
public render() {
const { classes, actions, visible } = this.props
return (
<Drawer
anchor="left"
className={classes.drawer}
open={visible}
variant="persistent"
>
<div
className={classes.paper}
tabIndex={0}
role="button"
>
<Typography className={classes.title} variant="h6" color="inherit">
<IconButton onClick={actions.toggleSettingsVisibility}>
<ChevronRight />
</IconButton>
Settings
</Typography>
<Divider />
{this.renderAutoExpand()}
{this.renderNodeOrder()}
{this.renderHighlightTopicUpdates()}
{this.selectTopicsOnMouseOver()}
{this.toggleTheme()}
</div>
<Tooltip placement="top" title="App Author">
<Typography className={classes.author} onClick={this.openGithubPage}>
by Thomas Nordquist
</Typography>
</Tooltip>
<BrokerStatistics />
</Drawer>
)
}
private openGithubPage = () => {
shell.openExternal('https://github.com/thomasnordquist')
}
@@ -232,6 +193,45 @@ class Settings extends React.Component<Props, {}> {
private onChangeSorting = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setTopicOrder(e.target.value as TopicOrder)
}
public render() {
const { classes, actions, visible } = this.props
return (
<Drawer
anchor="left"
className={classes.drawer}
open={visible}
variant="persistent"
>
<div
className={classes.paper}
tabIndex={0}
role="button"
>
<Typography className={classes.title} variant="h6" color="inherit">
<IconButton onClick={actions.toggleSettingsVisibility}>
<ChevronRight />
</IconButton>
Settings
</Typography>
<Divider />
{this.renderAutoExpand()}
{this.renderNodeOrder()}
{this.renderHighlightTopicUpdates()}
{this.selectTopicsOnMouseOver()}
{this.toggleTheme()}
</div>
<Tooltip placement="top" title="App Author">
<Typography className={classes.author} onClick={this.openGithubPage}>
by Thomas Nordquist
</Typography>
</Tooltip>
<BrokerStatistics />
</Drawer>
)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -29,6 +29,10 @@ class MessageHistory extends React.Component<Props, State> {
}
}
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
public renderHistory() {
const style = (element: HistoryItem) => ({
backgroundColor: element.selected ? this.props.theme.palette.action.selected : this.props.theme.palette.action.hover,
@@ -87,10 +91,6 @@ class MessageHistory extends React.Component<Props, State> {
</div>
)
}
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
}
const styles = (theme: Theme) => ({
@@ -98,7 +98,7 @@ const styles = (theme: Theme) => ({
backgroundColor: 'rgba(170, 170, 170, 0.2)',
marginTop: '16px',
},
badge: { right:'-25px' },
badge: { right: '-25px' },
})
export default withStyles(styles, { withTheme: true })(MessageHistory)

View File

@@ -56,6 +56,11 @@ interface State {
}
class Publish extends React.Component<Props, State> {
private editorOptions = {
showLineNumbers: false,
tabSize: 2,
}
constructor(props: any) {
super(props)
this.state = { history: [] }
@@ -88,7 +93,7 @@ class Publish extends React.Component<Props, State> {
}
}
private addMessageToHistory(topic: string, payload?: string) {
private addMessageToHistory(topic: string, payload?: string) {
// Remove duplicates
let filteredHistory = this.state.history.filter(e => e.payload !== payload || e.topic !== topic)
filteredHistory = filteredHistory.slice(-7)
@@ -96,16 +101,6 @@ class Publish extends React.Component<Props, State> {
this.setState({ history })
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
{this.editor()}
{this.history()}
</div>
)
}
private clearTopic = () => {
this.props.actions.setTopic('')
}
@@ -138,11 +133,6 @@ class Publish extends React.Component<Props, State> {
}
}
private editorOptions = {
showLineNumbers: false,
tabSize: 2,
}
private publishButton() {
return (
<Button
@@ -232,7 +222,7 @@ class Publish extends React.Component<Props, State> {
private onChangeQoS = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
if (value !== 0 && value !== 1 && value !== 2) {
if (value !== 0 && value !== 1 && value !== 2) {
return
}
@@ -326,6 +316,16 @@ class Publish extends React.Component<Props, State> {
/>
)
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
{this.editor()}
{this.history()}
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {

View File

@@ -46,26 +46,14 @@ class Sidebar extends React.Component<Props, State> {
this.setState(this.state)
}, 300)
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
constructor(props: any) {
super(props)
console.error('Find and fix me #state')
this.state = { node: new q.Tree(), valueRenderWidth: 300 }
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined })
}
}
public componentWillUnmount() {
this.props.node && this.removeUpdateListener(this.props.node)
}
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
node.onMerge.subscribe(this.updateNode)
node.onMessage.subscribe(this.updateNode)
@@ -76,18 +64,8 @@ class Sidebar extends React.Component<Props, State> {
node.onMessage.unsubscribe(this.updateNode)
}
public render() {
return (
<div id="Sidebar" className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
}
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
private renderTopicDeleteButton() {
if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
return null
}
@@ -179,6 +157,28 @@ class Sidebar extends React.Component<Props, State> {
</ExpansionPanelDetails>
)
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined })
}
}
public componentWillUnmount() {
this.props.node && this.removeUpdateListener(this.props.node)
}
public render() {
return (
<div id="Sidebar" className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -46,7 +46,7 @@ class Topic extends React.Component<Props, {}> {
>
{edge!.name}
</Button>
)],
)]
)
if (breadCrumps.length === 0) {
@@ -54,7 +54,7 @@ class Topic extends React.Component<Props, {}> {
}
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
prev.concat([<span key={key += 1}>/</span>]).concat(current),
prev.concat([<span key={key += 1}>/</span>]).concat(current)
)
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>

View File

@@ -22,14 +22,21 @@ interface State {
}
class MessageHistory extends React.Component<Props, State> {
private updateNode = throttle(() => {
this.setState(this.state)
}, 300)
constructor(props: any) {
super(props)
this.state = { }
}
private updateNode = throttle(() => {
this.setState(this.state)
}, 300)
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
if (message) {
this.props.onSelect(message)
}
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
@@ -82,20 +89,13 @@ class MessageHistory extends React.Component<Props, State> {
)
}
public renderPlot(data: {x: number, y: number}[]) {
public renderPlot(data: Array<{x: number, y: number}>) {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<PlotHistory data={data} />
</React.Suspense>
)
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
if (message) {
this.props.onSelect(message)
}
}
}
export default MessageHistory

View File

@@ -8,7 +8,7 @@ const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('re
const abbreviate = require('number-abbreviate')
interface Props {
data: {x: number, y: number}[]
data: Array<{x: number, y: number}>
}
interface Stats {
@@ -26,6 +26,23 @@ class PlotHistory extends React.Component<Props, Stats> {
this.setState({ width: width - 12 })
}
private hintFormatter = (point: any) => {
return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y },
]
}
private _forgetValue = () => {
this.setState({
value: undefined,
})
}
private _rememberValue = (value: any) => {
this.setState({ value })
}
public render() {
const data = this.props.data
@@ -50,23 +67,6 @@ class PlotHistory extends React.Component<Props, Stats> {
</div>
)
}
private hintFormatter = (point: any) => {
return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y },
]
}
private _forgetValue = () => {
this.setState({
value: undefined,
})
}
private _rememberValue = (value: any) => {
this.setState({ value })
}
}
export default PlotHistory

View File

@@ -47,30 +47,6 @@ class ValuePanel extends React.Component<Props, State> {
this.state = { }
}
public render() {
const { node, classes } = this.props
const { detailsStyle, summaryStyle } = this.panelStyle()
const copyValue = (node && node.message && node.message.value) ? <Copy value={Base64Message.toUnicodeString(node.message.value)} /> : null
return (
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}>
{this.messageMetaInfo()}
<div>
<React.Suspense fallback={<div>Loading...</div>}>
{this.renderValue()}
</React.Suspense>
</div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} selected={this.state.compareMessage} node={this.props.node} /></div>
</ExpansionPanelDetails>
</ExpansionPanel>
)
}
private renderValue() {
const node = this.props.node
if (!node || !node.message) {
@@ -87,7 +63,7 @@ class ValuePanel extends React.Component<Props, State> {
}
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
return null
}
@@ -152,6 +128,30 @@ class ValuePanel extends React.Component<Props, State> {
this.setState({ compareMessage: undefined })
}
}
public render() {
const { node, classes } = this.props
const { detailsStyle, summaryStyle } = this.panelStyle()
const copyValue = (node && node.message && node.message.value) ? <Copy value={Base64Message.toUnicodeString(node.message.value)} /> : null
return (
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}>
{this.messageMetaInfo()}
<div>
<React.Suspense fallback={<div>Loading...</div>}>
{this.renderValue()}
</React.Suspense>
</div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} selected={this.state.compareMessage} node={this.props.node} /></div>
</ExpansionPanelDetails>
</ExpansionPanel>
)
}
}
const mapDispatchToProps = (dispatch: any) => {

View File

@@ -24,6 +24,36 @@ class ValueRenderer extends React.Component<Props, State> {
this.state = { width: 0 }
}
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
previous={previous}
current={current}
language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
/>
)
}
private messageToPrettyJson(str: string): string | undefined {
try {
const json = JSON.parse(str)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
}
private updateWidth = (width: number) => {
if (width !== this.state.width) {
this.setState({ width })
}
}
private renderRawValue(value: string, compare: string) {
return this.renderDiff(value, compare)
}
public render() {
return (
<div style={{ padding: '8px 0px 8px 8px' }}>
@@ -38,7 +68,7 @@ class ValueRenderer extends React.Component<Props, State> {
const previousMessages = messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
compareMessage = message
}
@@ -71,36 +101,6 @@ class ValueRenderer extends React.Component<Props, State> {
return this.renderDiff(current, compare, language)
}
}
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
previous={previous}
current={current}
language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
/>
)
}
private messageToPrettyJson(str: string): string | undefined {
try {
const json = JSON.parse(str)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
}
private updateWidth = (width: number) => {
if (width !== this.state.width) {
this.setState({ width })
}
}
private renderRawValue(value: string, compare: string) {
return this.renderDiff(value, compare)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -91,27 +91,6 @@ class TitleBar extends React.Component<Props, {}> {
this.state = { }
}
public render() {
const { actions, classes } = this.props
return (
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.settings.toggleSettingsVisibility}>
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography>
{this.renderSearch()}
<PauseButton />
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
<ConnectionHealthIndicator withBackground={true} />
</Toolbar>
</AppBar>
)
}
private renderSearch() {
const { classes, topicFilter } = this.props
@@ -138,6 +117,27 @@ class TitleBar extends React.Component<Props, {}> {
private onFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.props.actions.settings.filterTopics(event.target.value)
}
public render() {
const { actions, classes } = this.props
return (
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.settings.toggleSettingsVisibility}>
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography>
{this.renderSearch()}
<PauseButton />
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
<ConnectionHealthIndicator withBackground={true} />
</Toolbar>
</AppBar>
)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -42,6 +42,10 @@ class Tree extends React.PureComponent<Props, State> {
this.state = { lastUpdate: 0 }
}
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
public time(): number {
const time = performance.now() - this.perf
this.perf = performance.now()
@@ -121,10 +125,6 @@ class Tree extends React.PureComponent<Props, State> {
</div>
)
}
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
}
const mapStateToProps = (state: AppState) => {

View File

@@ -75,17 +75,9 @@ class TreeNode extends React.Component<Props, State> {
private willUpdateTime: number = performance.now()
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
private subnodesDidchange = () => {
this.dirtySubnodes = true
}
private messageDidChange = () => {
this.dirtyMessage = true
}
private edgesDidChange = () => {
this.dirtyMessage = true
}
private setHover = debounce((hover: boolean) => {
this.setState({ mouseOver: hover })
}, 45)
constructor(props: Props) {
super(props)
@@ -97,9 +89,16 @@ class TreeNode extends React.Component<Props, State> {
}
}
public componentDidMount() {
const { treeNode } = this.props
this.addSubscriber(treeNode)
private subnodesDidchange = () => {
this.dirtySubnodes = true
}
private messageDidChange = () => {
this.dirtyMessage = true
}
private edgesDidChange = () => {
this.dirtyMessage = true
}
private addSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
@@ -122,49 +121,14 @@ class TreeNode extends React.Component<Props, State> {
treeNode.onMessage.unsubscribe(this.messageDidChange)
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.treeNode !== this.props.treeNode) {
this.removeSubscriber(this.props.treeNode)
this.addSubscriber(nextProps.treeNode)
}
}
public componentWillUnmount() {
const { treeNode } = this.props
this.removeSubscriber(treeNode)
this.nodeRef = undefined
}
private stateHasChanged(newState: State) {
return this.state.collapsedOverride !== newState.collapsedOverride
|| this.state.mouseOver !== newState.mouseOver
return this.state.collapsedOverride !== newState.collapsedOverride
|| this.state.mouseOver !== newState.mouseOver
|| this.state.selected !== newState.selected
}
private propsHasChanged(newProps: Props) {
return this.props.autoExpandLimit !== newProps.autoExpandLimit
}
public shouldComponentUpdate(nextProps: Props, nextState: State) {
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
return this.stateHasChanged(nextState)
|| this.propsHasChanged(nextProps)
|| (this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes)
|| this.animationDirty
|| shouldRenderToRemoveCssAnimation
}
public componentDidUpdate() {
if (this.props.performanceCallback) {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
}
public componentWillUpdate() {
if (this.props.performanceCallback) {
this.willUpdateTime = performance.now()
}
return this.props.autoExpandLimit !== newProps.autoExpandLimit
}
private toggle() {
@@ -179,9 +143,97 @@ class TreeNode extends React.Component<Props, State> {
return this.props.treeNode.edgeCount() > this.props.autoExpandLimit
}
private didSelectTopic = () => {
this.props.didSelectTopic(this.props.treeNode)
}
private didClickTitle = (event: React.MouseEvent) => {
event.preventDefault()
this.props.didSelectTopic(this.props.treeNode)
this.toggle()
}
private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(true)
if (this.props.selectTopicWithMouseOver && this.props.treeNode && this.props.treeNode.message && this.props.treeNode.message.value) {
this.props.didSelectTopic(this.props.treeNode)
}
}
private mouseOut = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(false)
}
private toggleCollapsed = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
}
private renderNodes() {
if (this.collapsed()) {
return null
}
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
autoExpandLimit={this.props.autoExpandLimit}
topicOrder={this.props.topicOrder}
lastUpdate={this.props.treeNode.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
)
}
public componentDidMount() {
const { treeNode } = this.props
this.addSubscriber(treeNode)
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.treeNode !== this.props.treeNode) {
this.removeSubscriber(this.props.treeNode)
this.addSubscriber(nextProps.treeNode)
}
}
public componentWillUnmount() {
const { treeNode } = this.props
this.removeSubscriber(treeNode)
this.nodeRef = undefined
}
public shouldComponentUpdate(nextProps: Props, nextState: State) {
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
return this.stateHasChanged(nextState)
|| this.propsHasChanged(nextProps)
|| (this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes)
|| this.animationDirty
|| shouldRenderToRemoveCssAnimation
}
public componentDidUpdate() {
if (this.props.performanceCallback) {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
}
public componentWillUpdate() {
if (this.props.performanceCallback) {
this.willUpdateTime = performance.now()
}
}
public render() {
const { classes } = this.props
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot && this.props.highlightTopicUpdates
@@ -217,58 +269,6 @@ class TreeNode extends React.Component<Props, State> {
</div>
)
}
private didSelectTopic = () => {
this.props.didSelectTopic(this.props.treeNode)
}
private didClickTitle = (event: React.MouseEvent) => {
event.preventDefault()
this.props.didSelectTopic(this.props.treeNode)
this.toggle()
}
private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(true)
if (this.props.selectTopicWithMouseOver && this.props.treeNode && this.props.treeNode.message && this.props.treeNode.message.value) {
this.props.didSelectTopic(this.props.treeNode)
}
}
private mouseOut = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(false)
}
private setHover = debounce((hover: boolean) => {
this.setState({ mouseOver: hover })
}, 45)
private toggleCollapsed = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
}
private renderNodes() {
if (this.collapsed()) {
return null
}
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
autoExpandLimit={this.props.autoExpandLimit}
topicOrder={this.props.topicOrder}
lastUpdate={this.props.treeNode.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
)
}
}
export default withStyles(styles, { withTheme: true })(TreeNode)

View File

@@ -32,7 +32,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
this.state = { alreadyAdded: 10 }
}
private sortedNodes(): q.TreeNode<TopicViewModel>[] {
private sortedNodes(): Array<q.TreeNode<TopicViewModel>> {
const { topicOrder, treeNode } = this.props
let edges = treeNode.edgeArray

View File

@@ -16,18 +16,6 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
public render() {
const { classes, treeNode, style, className } = this.props
return (
<span
className={`${classes.title} ${className}`}
style={style}
>
<span className={classes.expander} onClick={this.props.toggleCollapsed}>{this.renderExpander()}</span>
{this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
private renderSourceEdge() {
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
@@ -57,6 +45,18 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
const messages = this.props.treeNode.leafMessageCount()
return <span className={this.props.classes.collapsedSubnodes}>({this.props.treeNode.childTopicCount()} topics, {messages} messages)</span>
}
public render() {
const { classes, treeNode, style, className } = this.props
return (
<span
className={`${classes.title} ${className}`}
style={style}
>
<span className={classes.expander} onClick={this.props.toggleCollapsed}>{this.renderExpander()}</span>
{this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
}
const styles = (theme: Theme) => ({

View File

@@ -13,13 +13,6 @@ const unitMapping = {
}
class DateFormatter extends React.Component<Props, {}> {
public render() {
const locale = window.navigator.language
if (this.props.intervalSince) {
return <span>{this.intervalSince(this.props.intervalSince)}</span>
}
return <span>{locale ? this.localizedDate(locale) : this.legacyDate()}</span>
}
private intervalSince(intervalSince: Date) {
const interval = intervalSince.getTime() - this.props.date.getTime()
@@ -49,6 +42,13 @@ class DateFormatter extends React.Component<Props, {}> {
return 's'
}
public render() {
const locale = window.navigator.language
if (this.props.intervalSince) {
return <span>{this.intervalSince(this.props.intervalSince)}</span>
}
return <span>{locale ? this.localizedDate(locale) : this.legacyDate()}</span>
}
}
export default DateFormatter

View File

@@ -26,9 +26,9 @@ const store = createStore(
composeEnhancers(
applyMiddleware(
reduxThunk,
batchDispatchMiddleware,
),
),
batchDispatchMiddleware
)
)
)
function createTheme(type: 'light' | 'dark') {
@@ -77,5 +77,5 @@ ReactDOM.render(
<Provider store={store}>
<Application />
</Provider>,
document.getElementById('app'),
document.getElementById('app')
)

View File

@@ -3,7 +3,7 @@ import { ConnectionOptions } from '../model/ConnectionOptions'
import { createReducer } from './lib'
export interface ConnectionManagerState {
connections: {[s:string]: ConnectionOptions},
connections: {[s: string]: ConnectionOptions},
selected?: string
showAdvancedSettings: boolean
}
@@ -29,7 +29,7 @@ export enum ActionTypes {
export interface SetConnections {
type: ActionTypes.CONNECTION_MANAGER_SET_CONNECTIONS
connections: {[s:string]: ConnectionOptions}
connections: {[s: string]: ConnectionOptions}
}
export interface SelectConnection {

View File

@@ -48,35 +48,35 @@ const globalState: Reducer<GlobalState | undefined, CustomAction> = (state = ini
trackEvent(action.type)
console.log(action.type)
switch (action.type) {
case ActionTypes.showUpdateNotification:
return {
case ActionTypes.showUpdateNotification:
return {
...state,
showUpdateNotification: action.showUpdateNotification,
}
case ActionTypes.showError:
return {
case ActionTypes.showError:
return {
...state,
error: action.error,
}
case ActionTypes.didLaunch:
return {
case ActionTypes.didLaunch:
return {
...state,
launching: false,
}
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
}
return {
return {
...state,
showUpdateDetails: action.showUpdateDetails,
}
default:
return state
default:
return state
}
}