Skip to content

Commit a058c2c

Browse files
committed
add time series chart
1 parent df9fec6 commit a058c2c

File tree

10 files changed

+193
-186
lines changed

10 files changed

+193
-186
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dlphn"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
authors = ["Todd Treece <[email protected]>"]
55
description = "a humble sensor data logger."
66
homepage = "https://github.com/toddtreece/dlphn-rs"

ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "dlphn-ui",
33
"version": "0.1.0",
44
"private": true,
5+
"proxy": "http://localhost:8080/",
56
"dependencies": {
67
"@testing-library/jest-dom": "^4.2.4",
78
"@testing-library/react": "^9.4.0",
@@ -13,6 +14,7 @@
1314
"@types/react-redux": "^7.1.5",
1415
"@types/react-router": "^5.1.4",
1516
"@types/react-router-dom": "^5.1.3",
17+
"@types/recharts": "^1.8.5",
1618
"@types/redux": "^3.6.0",
1719
"@typescript-eslint/eslint-plugin": "^2.15.0",
1820
"@typescript-eslint/parser": "^2.15.0",
@@ -26,7 +28,6 @@
2628
"react-router": "^5.1.2",
2729
"react-router-dom": "^5.1.2",
2830
"react-scripts": "3.3.0",
29-
"react-vis": "^1.11.7",
3031
"recharts": "^2.0.0-beta.1",
3132
"redux": "^4.0.5",
3233
"redux-devtools-extension": "^2.13.8",

ui/src/components/data/Chart.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { Tooltip, ResponsiveContainer, LineChart, Line, XAxis, YAxis } from 'recharts';
3+
import moment from 'moment';
4+
5+
import { DataPayload } from '../../store/data/types';
6+
7+
interface DataProps {
8+
columns: string[];
9+
chart: DataPayload[];
10+
}
11+
12+
export const DataChart: React.FC<DataProps> = props => {
13+
const { columns, chart } = props;
14+
const [created, ...lines] = columns;
15+
16+
return (
17+
<ResponsiveContainer height={300} width="95%">
18+
<LineChart data={chart} style={{ marginTop: '2em' }}>
19+
<XAxis
20+
domain={['auto', 'auto']}
21+
name="Time"
22+
tickFormatter={time => moment(time).format('MM/DD HH:mm')}
23+
reversed={true}
24+
dataKey="created"
25+
/>
26+
<YAxis />
27+
<Tooltip labelFormatter={time => moment(time).format('MM/DD/YY HH:mm:SS')} />
28+
{lines.map(name => (
29+
<Line type="monotone" stroke="#8884d8" key={`datatable-${name}`} dataKey={name} />
30+
))}
31+
</LineChart>
32+
</ResponsiveContainer>
33+
);
34+
};

ui/src/components/data/Table.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import { Table, Popup } from 'semantic-ui-react';
3+
import { History } from 'history';
4+
import moment from 'moment';
5+
6+
import { Data } from '../../client';
7+
8+
interface DataProps {
9+
streamKey: string;
10+
history: History;
11+
columns: string[];
12+
data: Data[];
13+
}
14+
15+
export const DataTable: React.FC<DataProps> = props => {
16+
const { history, streamKey: key, columns, data } = props;
17+
18+
return (
19+
<Table celled fixed>
20+
<Table.Header>
21+
<Table.Row>
22+
{columns.map((column: string) => (
23+
<Table.HeaderCell key={column}>{column}</Table.HeaderCell>
24+
))}
25+
</Table.Row>
26+
</Table.Header>
27+
28+
<Table.Body>
29+
{data.map((datum: Data) => (
30+
<Table.Row key={datum.id} onClick={() => history.push(`/streams/${key}/data`)}>
31+
<Table.Cell>
32+
<Popup
33+
size="mini"
34+
content={datum.created}
35+
trigger={<div>{moment(datum.created).fromNow()}</div>}
36+
inverted
37+
/>
38+
</Table.Cell>
39+
{Object.values(datum.payload || {}).map((v, i) => {
40+
return <Table.Cell key={`data-cell-${datum.id}-${i}`}>{v}</Table.Cell>;
41+
})}
42+
</Table.Row>
43+
))}
44+
</Table.Body>
45+
</Table>
46+
);
47+
};

ui/src/pages/Data.tsx

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React, { useEffect } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
3-
import { Table, Header, Popup } from 'semantic-ui-react';
3+
import { Breadcrumb } from 'semantic-ui-react';
44
import { match, Link } from 'react-router-dom';
55
import { History } from 'history';
6-
import moment from 'moment';
76

87
import { ApplicationState } from '../store';
9-
import { Data } from '../client';
108
import { fetchRequest, startSubscription, endSubscription } from '../store/data/actions';
9+
import { DataTable } from '../components/data/Table';
10+
import { DataChart } from '../components/data/Chart';
1111

1212
interface RouteInfo {
1313
key: string;
@@ -19,61 +19,35 @@ interface MainProps {
1919
}
2020

2121
export const DataPage: React.FC<MainProps> = props => {
22+
const { loading, columns, data, chart } = useSelector((state: ApplicationState) => state.data);
23+
const dispatch = useDispatch();
2224
const {
23-
match: { params }
25+
history,
26+
match: {
27+
params: { key }
28+
}
2429
} = props;
25-
const { loading, data } = useSelector((state: ApplicationState) => state.data);
26-
const dispatch = useDispatch();
2730

2831
useEffect(() => {
29-
dispatch(fetchRequest(params.key));
30-
dispatch(startSubscription(params.key));
32+
dispatch(fetchRequest(key));
33+
dispatch(startSubscription(key));
3134
return () => {
32-
dispatch(endSubscription(params.key));
35+
dispatch(endSubscription(key));
3336
};
34-
}, [dispatch]);
35-
36-
// TODO tjt: move data formatting to store
37-
const payloadColumnsSet = new Set(data.map((datum: Data) => Object.keys(datum.payload || {})).flat());
38-
const payloadColumns = [...payloadColumnsSet].sort();
37+
}, [dispatch, key]);
3938

4039
return (
4140
<>
42-
<Header as="h1">
43-
dlphn.<Link to="/streams">streams</Link>.{params.key}
44-
</Header>
41+
<Breadcrumb size="big">
42+
<Breadcrumb.Section as={Link} link to="/streams">
43+
Streams
44+
</Breadcrumb.Section>
45+
<Breadcrumb.Divider icon="right chevron" />
46+
<Breadcrumb.Section>{key}</Breadcrumb.Section>
47+
</Breadcrumb>
4548
{!loading && data.length === 0 && <div>not found.</div>}
46-
{data.length > 0 && (
47-
<Table celled fixed>
48-
<Table.Header>
49-
<Table.Row>
50-
<Table.HeaderCell>created</Table.HeaderCell>
51-
{payloadColumns.map((column: string) => (
52-
<Table.HeaderCell>{column}</Table.HeaderCell>
53-
))}
54-
</Table.Row>
55-
</Table.Header>
56-
57-
<Table.Body>
58-
{data.map((datum: Data) => (
59-
<Table.Row key={datum.id} onClick={() => props.history.push(`/streams/${params.key}/data`)}>
60-
<Table.Cell>
61-
<Popup
62-
size="mini"
63-
content={datum.created}
64-
trigger={<div>{moment(datum.created).fromNow()}</div>}
65-
inverted
66-
/>
67-
</Table.Cell>
68-
{payloadColumns.map((column: string) => {
69-
const payload = datum.payload || {};
70-
return <Table.Cell>{payload[column] || ''}</Table.Cell>;
71-
})}
72-
</Table.Row>
73-
))}
74-
</Table.Body>
75-
</Table>
76-
)}
49+
{chart.length > 0 && <DataChart columns={columns} chart={chart}></DataChart>}
50+
{data.length > 0 && <DataTable streamKey={key} columns={columns} data={data} history={history}></DataTable>}
7751
</>
7852
);
7953
};

ui/src/pages/Streams.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
3-
import { Table, Header, Popup, Label } from 'semantic-ui-react';
3+
import { Table, Breadcrumb, Popup, Label } from 'semantic-ui-react';
44
import { History } from 'history';
55
import moment from 'moment';
66

@@ -22,7 +22,9 @@ export const StreamsPage: React.FC<MainProps> = ({ history }) => {
2222

2323
return (
2424
<>
25-
<Header as="h1">dlphn.streams</Header>
25+
<Breadcrumb size="big">
26+
<Breadcrumb.Section>Streams</Breadcrumb.Section>
27+
</Breadcrumb>
2628
<Table celled selectable>
2729
<Table.Header>
2830
<Table.Row>

ui/src/store/data/reducer.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,69 @@
11
import { Reducer } from 'redux';
2-
import { DataActionTypes, DataState } from './types';
2+
import { DataActionTypes, DataState, DataPayload } from './types';
3+
import { Data } from '../../client';
34

45
export const initialState: DataState = {
56
key: '',
7+
columns: [],
8+
chart: [],
69
data: [],
710
errors: undefined,
811
loading: false
912
};
1013

14+
const formatColumns = (data: Data[]) => {
15+
const columns = data.map(({ payload }) => Object.keys(payload || {})).flat();
16+
const sortedUniqueColumns = [...new Set(columns)].sort();
17+
return ['created', ...sortedUniqueColumns];
18+
};
19+
20+
const formatPayloads = (data: Data[]) => {
21+
const columns = formatColumns(data);
22+
const [_, ...payloadColumns] = columns;
23+
const defaultPayload = payloadColumns.reduce((p: DataPayload, c: string) => ({ ...p, [c]: '' }), {});
24+
25+
const formattedData = data.reduce(
26+
(acc: { data: Data[]; chart: DataPayload[] }, datum: Data) => {
27+
const payload = {
28+
...defaultPayload,
29+
...datum.payload
30+
};
31+
32+
acc.data.push({
33+
...datum,
34+
payload
35+
});
36+
37+
acc.chart.push({
38+
...payload,
39+
created: new Date(datum.created || Date.now()).getTime()
40+
});
41+
42+
return acc;
43+
},
44+
{ data: [], chart: [] }
45+
);
46+
47+
return {
48+
columns,
49+
...formattedData
50+
};
51+
};
52+
1153
const reducer: Reducer<DataState> = (state = initialState, action) => {
1254
switch (action.type) {
1355
case DataActionTypes.FETCH_REQUEST: {
1456
return { ...state, loading: true, key: action.payload };
1557
}
1658
case DataActionTypes.FETCH_SUCCESS: {
17-
return { ...state, loading: false, data: action.payload };
59+
return { ...state, loading: false, ...formatPayloads(action.payload) };
1860
}
1961
case DataActionTypes.FETCH_ERROR: {
2062
return { ...state, loading: false, errors: action.payload };
2163
}
2264
case DataActionTypes.NEW_MESSAGE: {
23-
return { ...state, data: [action.payload, ...state.data] };
65+
const data = [action.payload, ...state.data];
66+
return { ...state, ...formatPayloads(data) };
2467
}
2568
case DataActionTypes.START_SUBSCRIPTION: {
2669
return state;

ui/src/store/data/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ export enum DataActionTypes {
99
END_SUBSCRIPTION = '@@data/END_SUBSCRIPTION'
1010
}
1111

12+
export type DataPayload = { [key: string]: string | Date | Number };
13+
1214
export interface DataState {
1315
readonly key: string;
1416
readonly loading: boolean;
17+
readonly columns: string[];
1518
readonly data: Data[];
19+
readonly chart: DataPayload[];
1620
readonly errors?: string;
1721
}

0 commit comments

Comments
 (0)