Skip to content

Commit

Permalink
add time series chart
Browse files Browse the repository at this point in the history
  • Loading branch information
toddtreece committed Jan 18, 2020
1 parent df9fec6 commit a058c2c
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 186 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dlphn"
version = "0.4.0"
version = "0.5.0"
authors = ["Todd Treece <[email protected]>"]
description = "a humble sensor data logger."
homepage = "https://github.com/toddtreece/dlphn-rs"
Expand Down
3 changes: 2 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "dlphn-ui",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:8080/",
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
Expand All @@ -13,6 +14,7 @@
"@types/react-redux": "^7.1.5",
"@types/react-router": "^5.1.4",
"@types/react-router-dom": "^5.1.3",
"@types/recharts": "^1.8.5",
"@types/redux": "^3.6.0",
"@typescript-eslint/eslint-plugin": "^2.15.0",
"@typescript-eslint/parser": "^2.15.0",
Expand All @@ -26,7 +28,6 @@
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
"react-vis": "^1.11.7",
"recharts": "^2.0.0-beta.1",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.8",
Expand Down
34 changes: 34 additions & 0 deletions ui/src/components/data/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { Tooltip, ResponsiveContainer, LineChart, Line, XAxis, YAxis } from 'recharts';
import moment from 'moment';

import { DataPayload } from '../../store/data/types';

interface DataProps {
columns: string[];
chart: DataPayload[];
}

export const DataChart: React.FC<DataProps> = props => {
const { columns, chart } = props;
const [created, ...lines] = columns;

return (
<ResponsiveContainer height={300} width="95%">
<LineChart data={chart} style={{ marginTop: '2em' }}>
<XAxis
domain={['auto', 'auto']}
name="Time"
tickFormatter={time => moment(time).format('MM/DD HH:mm')}
reversed={true}
dataKey="created"
/>
<YAxis />
<Tooltip labelFormatter={time => moment(time).format('MM/DD/YY HH:mm:SS')} />
{lines.map(name => (
<Line type="monotone" stroke="#8884d8" key={`datatable-${name}`} dataKey={name} />
))}
</LineChart>
</ResponsiveContainer>
);
};
47 changes: 47 additions & 0 deletions ui/src/components/data/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { Table, Popup } from 'semantic-ui-react';
import { History } from 'history';
import moment from 'moment';

import { Data } from '../../client';

interface DataProps {
streamKey: string;
history: History;
columns: string[];
data: Data[];
}

export const DataTable: React.FC<DataProps> = props => {
const { history, streamKey: key, columns, data } = props;

return (
<Table celled fixed>
<Table.Header>
<Table.Row>
{columns.map((column: string) => (
<Table.HeaderCell key={column}>{column}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>

<Table.Body>
{data.map((datum: Data) => (
<Table.Row key={datum.id} onClick={() => history.push(`/streams/${key}/data`)}>
<Table.Cell>
<Popup
size="mini"
content={datum.created}
trigger={<div>{moment(datum.created).fromNow()}</div>}
inverted
/>
</Table.Cell>
{Object.values(datum.payload || {}).map((v, i) => {
return <Table.Cell key={`data-cell-${datum.id}-${i}`}>{v}</Table.Cell>;
})}
</Table.Row>
))}
</Table.Body>
</Table>
);
};
70 changes: 22 additions & 48 deletions ui/src/pages/Data.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Table, Header, Popup } from 'semantic-ui-react';
import { Breadcrumb } from 'semantic-ui-react';
import { match, Link } from 'react-router-dom';
import { History } from 'history';
import moment from 'moment';

import { ApplicationState } from '../store';
import { Data } from '../client';
import { fetchRequest, startSubscription, endSubscription } from '../store/data/actions';
import { DataTable } from '../components/data/Table';
import { DataChart } from '../components/data/Chart';

interface RouteInfo {
key: string;
Expand All @@ -19,61 +19,35 @@ interface MainProps {
}

export const DataPage: React.FC<MainProps> = props => {
const { loading, columns, data, chart } = useSelector((state: ApplicationState) => state.data);
const dispatch = useDispatch();
const {
match: { params }
history,
match: {
params: { key }
}
} = props;
const { loading, data } = useSelector((state: ApplicationState) => state.data);
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchRequest(params.key));
dispatch(startSubscription(params.key));
dispatch(fetchRequest(key));
dispatch(startSubscription(key));
return () => {
dispatch(endSubscription(params.key));
dispatch(endSubscription(key));
};
}, [dispatch]);

// TODO tjt: move data formatting to store
const payloadColumnsSet = new Set(data.map((datum: Data) => Object.keys(datum.payload || {})).flat());
const payloadColumns = [...payloadColumnsSet].sort();
}, [dispatch, key]);

return (
<>
<Header as="h1">
dlphn.<Link to="/streams">streams</Link>.{params.key}
</Header>
<Breadcrumb size="big">
<Breadcrumb.Section as={Link} link to="/streams">
Streams
</Breadcrumb.Section>
<Breadcrumb.Divider icon="right chevron" />
<Breadcrumb.Section>{key}</Breadcrumb.Section>
</Breadcrumb>
{!loading && data.length === 0 && <div>not found.</div>}
{data.length > 0 && (
<Table celled fixed>
<Table.Header>
<Table.Row>
<Table.HeaderCell>created</Table.HeaderCell>
{payloadColumns.map((column: string) => (
<Table.HeaderCell>{column}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>

<Table.Body>
{data.map((datum: Data) => (
<Table.Row key={datum.id} onClick={() => props.history.push(`/streams/${params.key}/data`)}>
<Table.Cell>
<Popup
size="mini"
content={datum.created}
trigger={<div>{moment(datum.created).fromNow()}</div>}
inverted
/>
</Table.Cell>
{payloadColumns.map((column: string) => {
const payload = datum.payload || {};
return <Table.Cell>{payload[column] || ''}</Table.Cell>;
})}
</Table.Row>
))}
</Table.Body>
</Table>
)}
{chart.length > 0 && <DataChart columns={columns} chart={chart}></DataChart>}
{data.length > 0 && <DataTable streamKey={key} columns={columns} data={data} history={history}></DataTable>}
</>
);
};
6 changes: 4 additions & 2 deletions ui/src/pages/Streams.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Table, Header, Popup, Label } from 'semantic-ui-react';
import { Table, Breadcrumb, Popup, Label } from 'semantic-ui-react';
import { History } from 'history';
import moment from 'moment';

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

return (
<>
<Header as="h1">dlphn.streams</Header>
<Breadcrumb size="big">
<Breadcrumb.Section>Streams</Breadcrumb.Section>
</Breadcrumb>
<Table celled selectable>
<Table.Header>
<Table.Row>
Expand Down
49 changes: 46 additions & 3 deletions ui/src/store/data/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,69 @@
import { Reducer } from 'redux';
import { DataActionTypes, DataState } from './types';
import { DataActionTypes, DataState, DataPayload } from './types';
import { Data } from '../../client';

export const initialState: DataState = {
key: '',
columns: [],
chart: [],
data: [],
errors: undefined,
loading: false
};

const formatColumns = (data: Data[]) => {
const columns = data.map(({ payload }) => Object.keys(payload || {})).flat();
const sortedUniqueColumns = [...new Set(columns)].sort();
return ['created', ...sortedUniqueColumns];
};

const formatPayloads = (data: Data[]) => {
const columns = formatColumns(data);
const [_, ...payloadColumns] = columns;
const defaultPayload = payloadColumns.reduce((p: DataPayload, c: string) => ({ ...p, [c]: '' }), {});

const formattedData = data.reduce(
(acc: { data: Data[]; chart: DataPayload[] }, datum: Data) => {
const payload = {
...defaultPayload,
...datum.payload
};

acc.data.push({
...datum,
payload
});

acc.chart.push({
...payload,
created: new Date(datum.created || Date.now()).getTime()
});

return acc;
},
{ data: [], chart: [] }
);

return {
columns,
...formattedData
};
};

const reducer: Reducer<DataState> = (state = initialState, action) => {
switch (action.type) {
case DataActionTypes.FETCH_REQUEST: {
return { ...state, loading: true, key: action.payload };
}
case DataActionTypes.FETCH_SUCCESS: {
return { ...state, loading: false, data: action.payload };
return { ...state, loading: false, ...formatPayloads(action.payload) };
}
case DataActionTypes.FETCH_ERROR: {
return { ...state, loading: false, errors: action.payload };
}
case DataActionTypes.NEW_MESSAGE: {
return { ...state, data: [action.payload, ...state.data] };
const data = [action.payload, ...state.data];
return { ...state, ...formatPayloads(data) };
}
case DataActionTypes.START_SUBSCRIPTION: {
return state;
Expand Down
4 changes: 4 additions & 0 deletions ui/src/store/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ export enum DataActionTypes {
END_SUBSCRIPTION = '@@data/END_SUBSCRIPTION'
}

export type DataPayload = { [key: string]: string | Date | Number };

export interface DataState {
readonly key: string;
readonly loading: boolean;
readonly columns: string[];
readonly data: Data[];
readonly chart: DataPayload[];
readonly errors?: string;
}
Loading

0 comments on commit a058c2c

Please sign in to comment.