diff --git a/app/package.json b/app/package.json index 5809976..fcd9f2b 100644 --- a/app/package.json +++ b/app/package.json @@ -19,7 +19,10 @@ "brace": "^0.11.1", "compare-versions": "^3.4.0", "copy-text-to-clipboard": "^1.0.4", + "d3": "^5.9.2", + "d3-shape": "^1.3.5", "diff": "^4.0.1", + "dot-prop": "^5.0.0", "electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist", "file-loader": "^3.0.1", "get-value": "^3.0.1", @@ -47,6 +50,7 @@ "uuid": "^3.3.2" }, "devDependencies": { + "@types/d3": "^5.7.2", "@types/diff": "^4.0.1", "@types/get-value": "^3.0.1", "@types/node": "^10.12.18", diff --git a/app/src/components/Sidebar/CodeDiff/Gutters.tsx b/app/src/components/Sidebar/CodeDiff/Gutters.tsx index 78d5cdf..7fd7e35 100644 --- a/app/src/components/Sidebar/CodeDiff/Gutters.tsx +++ b/app/src/components/Sidebar/CodeDiff/Gutters.tsx @@ -1,5 +1,7 @@ import * as diff from 'diff' import * as React from 'react' +import Add from '@material-ui/icons/Add' +import Remove from '@material-ui/icons/Remove' import ShowChart from '@material-ui/icons/ShowChart' import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser' import { lineChangeStyle, trimNewlineRight } from './util' @@ -10,28 +12,70 @@ interface Props { changes: Array, literalPositions: Array classes: any + className: string + showDiagram: (dotPath: string, target: EventTarget) => void + hideDiagram: () => void } const style = (theme: Theme) => { return { gutterLine: { - display: 'flex' as 'flex', textAlign: 'right' as 'right', paddingRight: theme.spacing(0.5), height: '16px', + width: '100%', + }, + icon: { + width: '12px', + height: '12px', + marginTop: '2px', + borderRadius: '50%', + '&:hover': { + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, + }, + }, + hover: { + }, } } -function tokensForLine(change: diff.Change, line: number, literalPositions: Array) { - let diagram = literalPositions[line] ? : '' +function ChartIcon(props: { classes: any, literal: JsonPropertyLocation, showDiagram: (dotPath: string, target: EventTarget) => void, hideDiagram: () => void }) { + const mouseOver = (event: React.MouseEvent) => { + event.stopPropagation() + event.preventDefault() + if ((event.target as Element).tagName !== 'path') { + props.showDiagram(props.literal.path, event.target) + } + } + + const mouseOut = (event: React.MouseEvent) => { + event.stopPropagation() + event.preventDefault() + + if ((event.target as Element).tagName !== 'path') { + props.hideDiagram() + } + } + + return ( + + ) +} + +function tokensForLine(change: diff.Change, line: number, props: Props) { + const { classes, literalPositions } = props + + const literal = literalPositions[line] + const diagram = literal ? : null if (change.added) { - return [diagram, '+'] + return [diagram, ] } else if (change.removed) { - return '-' + return [] } else { - return [diagram, ' '] + return [diagram,
] } } @@ -44,13 +88,15 @@ function Gutters(props: Props) { currentLine = !change.removed ? currentLine + 1 : currentLine return (
- {tokensForLine(change, currentLine, props.literalPositions)} + {tokensForLine(change, currentLine, props)}
) }) }).reduce((a, b) => a.concat(b), []) - return
{gutters}
+ return +
{gutters}
+
} export default withStyles(style)(Gutters) diff --git a/app/src/components/Sidebar/CodeDiff/index.tsx b/app/src/components/Sidebar/CodeDiff/index.tsx index a0f9a1a..d2c2495 100644 --- a/app/src/components/Sidebar/CodeDiff/index.tsx +++ b/app/src/components/Sidebar/CodeDiff/index.tsx @@ -1,16 +1,20 @@ import * as diff from 'diff' import * as Prism from 'prismjs' +import * as q from '../../../../../backend/src/Model' import * as React from 'react' import DiffCount from './DiffCount' -import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors' -import { literalsMappedByLines } from '../../../../../backend/src/JsonAstParser' -import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA' -import { Theme, withStyles } from '@material-ui/core' -import 'prismjs/components/prism-json' -import { trimNewlineRight, lineChangeStyle } from './util'; import Gutters from './Gutters' +import TopicPlot from '../TopicPlot' +import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors' +import { isPlottable, lineChangeStyle, trimNewlineRight } from './util' +import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser' +import { Theme, withStyles, Popper, Paper, Fade, Zoom } from '@material-ui/core' +import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA' +import 'prismjs/components/prism-json' +const throttle = require('lodash.throttle') interface Props { + messageHistory: q.MessageHistory previous: string current: string nameOfCompareMessage: string @@ -18,17 +22,45 @@ interface Props { classes: any } -class CodeDiff extends React.Component { +interface State { + diagram?: DiagramOptions +} + +interface DiagramOptions { + dotPath?: string + anchorEl?: EventTarget +} + +class CodeDiff extends React.Component { private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre ~ pre' }) + private updateDiagram = throttle((diagram?: DiagramOptions) => { + this.setState({ diagram }) + }, 200) + constructor(props: Props) { super(props) + this.state = {} + } + + private showDiagram(dotPath: string, target: EventTarget) { + this.updateDiagram({ + dotPath, + anchorEl: target, + }) + } + + private hideDiagram() { + this.updateDiagram(undefined) } public render() { const changes = diff.diffLines(this.props.previous, this.props.current) const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n') - const literalPositions = literalsMappedByLines(this.props.current) || [] + const literalPositions = ( + (literalsMappedByLines(this.props.current) || []) + .map((l: JsonPropertyLocation) => isPlottable(l.value) ? l : undefined) + ) as Array let lineNumber = 0 const code = changes.map((change, key) => { @@ -41,7 +73,7 @@ class CodeDiff extends React.Component { }) lineNumber += changedLines - return
{lines}
+ return [
{lines}
] } return trimNewlineRight(change.value) @@ -49,14 +81,32 @@ class CodeDiff extends React.Component { .map((line, idx) => { return
{line}
}) - }) + }).reduce((a, b) => a.concat(b), []) + + const { diagram } = this.state return (
-
+ this.showDiagram(dotPath, target)} + hideDiagram={() => this.hideDiagram()} + className={this.props.classes.gutters} + changes={changes} + literalPositions={literalPositions} />
{code}
+ + + + {diagram ? : } + + +
) @@ -70,7 +120,7 @@ const style = (theme: Theme) => { font: "12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace", display: 'inline-grid' as 'inline-grid', margin: '0', - padding: '1px 0 2px 0', + padding: '1px 0 0 0', } return { @@ -81,6 +131,7 @@ const style = (theme: Theme) => { height: '16px', }, codeWrapper: { + display: 'flex', maxHeight: '15em', overflow: 'auto', backgroundColor: `${codeBlockColors.background}`, diff --git a/app/src/components/Sidebar/CodeDiff/util.tsx b/app/src/components/Sidebar/CodeDiff/util.tsx index b203034..83b8892 100644 --- a/app/src/components/Sidebar/CodeDiff/util.tsx +++ b/app/src/components/Sidebar/CodeDiff/util.tsx @@ -6,9 +6,7 @@ export function trimNewlineRight(str: string) { return str } -const gutterBaseStyle = { - width: '100%', -} +const gutterBaseStyle = {} const additionStyle = { ...gutterBaseStyle, @@ -30,4 +28,29 @@ export function lineChangeStyle(change: Diff.Change) { } return gutterBaseStyle +} + +export function toPlottableValue(value: any): number | undefined { + if (typeof value === 'number') { + return value + } + + if (typeof value === 'boolean') { + return value ? 1 : 0 + } + + const isNumber = !isNaN(value) + const floatVal = parseFloat(value) + if (isNumber && !isNaN(floatVal)) { + return floatVal + } + + const intVal = parseInt(value) + if (isNumber && !isNaN(intVal)) { + return intVal + } +} + +export function isPlottable(value: any) { + return !isNaN(toPlottableValue(value) as any) } \ No newline at end of file diff --git a/app/src/components/Sidebar/ValueRenderer/PlotHistory.tsx b/app/src/components/Sidebar/PlotHistory.tsx similarity index 78% rename from app/src/components/Sidebar/ValueRenderer/PlotHistory.tsx rename to app/src/components/Sidebar/PlotHistory.tsx index 183d7ee..8ef3306 100644 --- a/app/src/components/Sidebar/ValueRenderer/PlotHistory.tsx +++ b/app/src/components/Sidebar/PlotHistory.tsx @@ -1,15 +1,15 @@ -import * as q from '../../../../../backend/src/Model' +import * as q from '../../../../backend/src/Model' import * as React from 'react' -import DateFormatter from '../../helper/DateFormatter' +import DateFormatter from '../helper/DateFormatter' import { default as ReactResizeDetector } from 'react-resize-detector' import 'react-vis/dist/style.css' -import { Base64Message } from '../../../../../backend/src/Model/Base64Message' -const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('react-vis') +const { XYPlot, LineMarkSeries, Hint, XAxis, YAxis, HorizontalGridLines } = require('react-vis') const abbreviate = require('number-abbreviate') interface Props { data: Array<{x: number, y: number}> } +// const configuredCurve = d3Shape.curveBundle.beta(1) interface Stats { width: number @@ -47,9 +47,10 @@ class PlotHistory extends React.Component { const data = this.props.data return ( -
- +
+ + abbreviate(num)} @@ -59,7 +60,7 @@ class PlotHistory extends React.Component { onValueMouseOut={this._forgetValue} size={3} data={data} - curve={data.length < 50 ? 'curveCardinal' : undefined} + curve="curveMonotoneX" /> {this.state.value ? : null} diff --git a/app/src/components/Sidebar/TopicPlot.tsx b/app/src/components/Sidebar/TopicPlot.tsx new file mode 100644 index 0000000..fe59ab8 --- /dev/null +++ b/app/src/components/Sidebar/TopicPlot.tsx @@ -0,0 +1,39 @@ +import * as dotProp from 'dot-prop' +import * as q from '../../../../backend/src/Model' +import * as React from 'react' +import PlotHistory from './PlotHistory' +import { Base64Message } from '../../../../backend/src/Model/Base64Message' +import { toPlottableValue } from './CodeDiff/util' + +interface Props { + history: q.MessageHistory + dotPath?: string +} + +function nodeToHistory(history: q.MessageHistory) { + return history + .toArray() + .map((message: q.Message) => { + const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN + return { x: message.received.getTime(), y: toPlottableValue(value) } + }).filter(data => !isNaN(data.y as any)) as any +} + +function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) { + return history + .toArray() + .map((message: q.Message) => { + const json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {} + let value = dotProp.get(json, dotPath) + + return { x: message.received.getTime(), y: toPlottableValue(value) } + }).filter(data => !isNaN(data.y as any)) as any +} + +function render(props: Props) { + const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history) + console.log(props.dotPath, data) + return +} + +export default render diff --git a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx index a9d4eb5..551f189 100644 --- a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx +++ b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx @@ -4,11 +4,11 @@ import BarChart from '@material-ui/icons/BarChart' import Copy from '../../helper/Copy' import DateFormatter from '../../helper/DateFormatter' import History from '../HistoryDrawer' +import TopicPlot from '../TopicPlot' import { Base64Message } from '../../../../../backend/src/Model/Base64Message' +import { isPlottable } from '../CodeDiff/util' import { TopicViewModel } from '../../../model/TopicViewModel' -const PlotHistory = React.lazy(() => import('./PlotHistory')) - const throttle = require('lodash.throttle') interface Props { @@ -76,33 +76,19 @@ class MessageHistory extends React.Component { return element }) - const numericMessages = history - .map((message: q.Message) => { - const value = message.value ? parseFloat(Base64Message.toUnicodeString(message.value)) : NaN - return { x: message.received.getTime(), y: value } - }).filter(data => !isNaN(data.y)) - const showPlot = numericMessages.length >= 2 - + const isMessagePlottable = node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value)) return (
: undefined} + contentTypeIndicator={isMessagePlottable ? : undefined} onClick={this.displayMessage} > - {showPlot ? this.renderPlot(numericMessages) : null} + {isMessagePlottable ? : null}
) } - - public renderPlot(data: Array<{x: number, y: number}>) { - return ( - Loading...
}> - - - ) - } } export default MessageHistory diff --git a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx index c365e51..e01c9aa 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx @@ -9,7 +9,7 @@ import { ValueRendererDisplayMode } from '../../../reducers/Settings' interface Props { message: q.Message - messageHistory: q.RingBuffer + messageHistory: q.MessageHistory compareWith?: q.Message renderMode: ValueRendererDisplayMode } @@ -27,6 +27,7 @@ class ValueRenderer extends React.Component { private renderDiff(current: string = '', previous: string = '', language?: 'json') { return ( diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index 1f6f548..e557eb2 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -1,12 +1,12 @@ import { Destroyable } from './Destroyable' -import { Edge, Message, RingBuffer } from './' +import { Edge, Message, RingBuffer, MessageHistory } from './' import { EventDispatcher, MqttMessage } from '../../../events' export class TreeNode { public sourceEdge?: Edge public message?: Message public mqttMessage?: MqttMessage - public messageHistory: RingBuffer = new RingBuffer(20000, 100) + public messageHistory: MessageHistory = new RingBuffer(20000, 100) public viewModel?: ViewModel public edges: {[s: string]: Edge} = {} public edgeArray: Array> = [] diff --git a/backend/src/Model/index.ts b/backend/src/Model/index.ts index a6bfe16..047e633 100644 --- a/backend/src/Model/index.ts +++ b/backend/src/Model/index.ts @@ -4,4 +4,5 @@ export { Message } from './Message' export { TreeNodeFactory } from './TreeNodeFactory' export { Tree } from './Tree' export { Hashable } from './Hashable' +export { MessageHistory } from './MessageHistory' export * from './RingBuffer'