Skip to content

Commit

Permalink
better caching, ui improvements, ui error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
the-butcher committed May 13, 2024
1 parent 8b45152 commit 088b355
Show file tree
Hide file tree
Showing 20 changed files with 459 additions and 219 deletions.
65 changes: 52 additions & 13 deletions moth_client/moth_client_p22/src/RootApp.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ShowChartIcon from '@mui/icons-material/ShowChart';
import TocIcon from '@mui/icons-material/Toc';
import TuneIcon from '@mui/icons-material/Tune';
import { CssBaseline, IconButton, Paper, Stack, ThemeProvider } from '@mui/material';
import { Alert, CssBaseline, IconButton, Paper, Snackbar, Stack, ThemeProvider } from '@mui/material';
import { useEffect, useRef, useState } from 'react';
import TabConfig from './components/TabConfig';
import TabServer from './components/TabServer';
Expand All @@ -14,6 +14,7 @@ import { DISP_CONFIG_DEFAULT } from './types/IDispConfig';
import { WIFI_CONFIG_DEFAULT } from './types/IWifiConfig';
import { MQTT_CONFIG_DEFAULT } from './types/IMqttConfig';
import { PiecewiseColorConfig } from '@mui/x-charts/models/colorMapping';
import { IMessage } from './types/ITabProps';

type VIEW_TYPE = 'values' | 'config' | 'server';

Expand All @@ -23,11 +24,33 @@ const RootApp = () => {
const boxUrl = `https://192.168.0.66/api`; // when running directly from device

/**
* TODO :: strategy for not having to re-evaluate (lots of http requests) date range often and for keeping historic data in cache
* TODO :: create form around device configuration (display, wifi, mqtt) and implement reassembly of those values for re-upload to device
* TODO :: initial step to establish and validate connection (i.e. do not show an empty chart)
*/

const [viewType, setViewType] = useState<VIEW_TYPE>('values');
const [alertMessage, setAlertMessage] = useState<IMessage>({
message: '',
severity: 'success',
active: false
});

const handleClose = (event: React.SyntheticEvent | Event, reason?: string) => {
if (reason === 'clickaway') {
return;
}
setAlertMessage({
...alertMessage,
active: false
})
};

const handleAlertMessage = (_alertMessage: IMessage) => {

console.debug(`📞 handling alert message`, _alertMessage);

setAlertMessage(_alertMessage);

}

const handleValuesUpdate = (updates: Partial<ITabValuesProps>) => {

Expand Down Expand Up @@ -56,7 +79,7 @@ const RootApp = () => {

// updates to the display config
let seriesDefUpdateRequired = false;
if (!compare(updates.disp.co2, tabConfigPropsRef.current.disp.co2, ['wHi', 'rHi'])) {
if (updates.disp && !compare(updates.disp.co2, tabConfigPropsRef.current.disp.co2, ['wHi', 'rHi'])) {
seriesDefUpdateRequired = true;
(SERIES_DEFS.co2Lpf.colorMap as PiecewiseColorConfig).thresholds = [
updates.disp.co2.wHi,
Expand All @@ -67,17 +90,16 @@ const RootApp = () => {
updates.disp.co2.rHi
];
}
if (!compare(updates.disp.deg, tabConfigPropsRef.current.disp.deg, ['rLo', 'wLo', 'wHi', 'rHi'])) {
if (updates.disp && !compare(updates.disp.deg, tabConfigPropsRef.current.disp.deg, ['rLo', 'wLo', 'wHi', 'rHi'])) {
seriesDefUpdateRequired = true;
(SERIES_DEFS.deg.colorMap as PiecewiseColorConfig).thresholds = [
updates.disp.deg.rLo,
updates.disp.deg.wLo,
updates.disp.deg.wHi,
updates.disp.deg.rHi
];
console.log('SERIES_DEFS.deg.colorMap', SERIES_DEFS.deg.colorMap);
}
if (!compare(updates.disp.hum, tabConfigPropsRef.current.disp.hum, ['rLo', 'wLo', 'wHi', 'rHi'])) {
if (updates.disp && !compare(updates.disp.hum, tabConfigPropsRef.current.disp.hum, ['rLo', 'wLo', 'wHi', 'rHi'])) {
seriesDefUpdateRequired = true;
(SERIES_DEFS.hum.colorMap as PiecewiseColorConfig).thresholds = [
updates.disp.hum.rLo,
Expand Down Expand Up @@ -116,7 +138,8 @@ const RootApp = () => {
},
records: [],
seriesDef: SERIES_DEFS.co2Lpf,
handleUpdate: handleValuesUpdate
handleUpdate: handleValuesUpdate,
handleAlertMessage
});
const [tabValuesProps, setTabValuesProps] = useState<ITabValuesProps>(tabValuesPropsRef.current);

Expand All @@ -125,7 +148,8 @@ const RootApp = () => {
disp: DISP_CONFIG_DEFAULT,
wifi: WIFI_CONFIG_DEFAULT,
mqtt: MQTT_CONFIG_DEFAULT,
handleUpdate: handleConfigUpdate
handleUpdate: handleConfigUpdate,
handleAlertMessage
});
const [tabConfigProps, setTabConfigProps] = useState<ITabConfigProps>(tabConfigPropsRef.current);

Expand All @@ -137,22 +161,37 @@ const RootApp = () => {
return (
<ThemeProvider theme={ThemeUtil.createTheme()}>
<CssBaseline />
<Snackbar
open={alertMessage.active}
// autoHideDuration={5000}
onClose={handleClose}
sx={{ left: '72px' }}
>
<Alert
onClose={handleClose}
severity={alertMessage.severity}
variant='filled'
sx={{ width: '100%' }}
>
{alertMessage.message}
</Alert>
</Snackbar>
<Stack direction={'row'} spacing={0} sx={{ height: '100%' }}>
<Paper elevation={4} sx={{ display: 'flex', flexDirection: 'column', position: 'fixed', marginTop: '5px', backgroundColor: '#FAFAFA', border: '1px solid #DDDDDD' }}>
<IconButton size='small' title='data' onClick={() => setViewType('values')}>
<IconButton size='small' title='data' onClick={() => setViewType('values')} sx={{ color: viewType === 'values' ? 'black' : 'gray' }}>
<ShowChartIcon />
</IconButton>
<IconButton size='small' title='configuration' onClick={() => setViewType('config')}>
<IconButton size='small' title='configuration' onClick={() => setViewType('config')} sx={{ color: viewType === 'config' ? 'black' : 'gray' }}>
<TuneIcon />
</IconButton>
<IconButton size='small' title='server api' onClick={() => setViewType('server')}>
<IconButton size='small' title='server api' onClick={() => setViewType('server')} sx={{ color: viewType === 'server' ? 'black' : 'gray' }}>
<TocIcon />
</IconButton>
</Paper>
<div style={{ minWidth: '45px' }}></div>
<TabValues {...tabValuesProps} style={{ display: viewType === 'values' ? 'flex' : 'none' }} />
<TabConfig {...tabConfigProps} style={{ display: viewType === 'config' ? 'flex' : 'none' }} />
<TabServer boxUrl={boxUrl} style={{ display: viewType === 'server' ? 'flex' : 'none' }} />
<TabServer boxUrl={boxUrl} handleAlertMessage={handleAlertMessage} style={{ display: viewType === 'server' ? 'flex' : 'none' }} />
</Stack >
</ThemeProvider >
);
Expand Down
4 changes: 3 additions & 1 deletion moth_client/moth_client_p22/src/components/ApiCalcsv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import ApiResponse from './ApiResponse';
import { IApiProperties } from '../types/IApiProperties';
import { IResponseProps } from '../types/IResponseProps';


/**
* @deprecated
*/
const ApiCalcsv = (props: IApiProperties) => {

const apiName = 'calcsv';
Expand Down
6 changes: 3 additions & 3 deletions moth_client/moth_client_p22/src/components/ApiResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const ApiResponse = (props: IResponseProps) => {
}, [time, href, http, data]);

return (
<div>
<div style={{ width: 'inherit' }}>
<Typography variant="caption" sx={{ display: "flex", alignItems: "center" }}>
<QueryBuilderIcon sx={{ fontSize: 'medium' }} />
&nbsp;{new Date(time).toLocaleTimeString()}
Expand All @@ -37,8 +37,8 @@ const ApiResponse = (props: IResponseProps) => {
}
&nbsp;{type}
</Typography>
<Typography variant="caption" sx={{ display: "flex", alignItems: "center", height: '200px', background: 'black', color: 'white', marginTop: '4px' }}>
<pre style={{ height: 'inherit', width: '100%', overflow: 'auto', padding: '6px' }}>{content}</pre>
<Typography variant="caption" component={'div'} sx={{ display: "flex", alignItems: "center", width: '100%', color: 'white', marginTop: '4px' }}>
<pre style={{ whiteSpace: 'pre-wrap', height: '200px', padding: '6px', overflow: 'auto', background: 'black', width: '100%', display: 'block' }}>{content}</pre>
</Typography>
</div>

Expand Down
101 changes: 82 additions & 19 deletions moth_client/moth_client_p22/src/components/ChartValues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { useEffect, useRef, useState } from "react";
import { IChartProps } from "../types/IChartProps";
import { TICK_DEFINITIONS } from "../types/ITickDefinition";
import { TimeUtil } from "../util/TimeUtil";
import { IRecord } from "../types/IRecord";

const ChartValues = (props: IChartProps) => {

const { width, height, seriesDef, records, exportTo, handleExportComplete } = props;

const [tickDefIndex, setTickDefIndex] = useState<number>(0);
const [tickInterval, setTickInterval] = useState<number[]>();
const [stepRecords, setStepRecords] = useState<IRecord[]>([]);
const chartRef = useRef<SVGElement>();

/**
Expand Down Expand Up @@ -97,22 +100,23 @@ const ChartValues = (props: IChartProps) => {
const maxTickCount = chartWidth / 15;
// console.log('chartWidth', chartWidth, 'maxTickCount', maxTickCount, 'dpr', window.devicePixelRatio);

let tickDefinitionIndex = 0;
for (; tickDefinitionIndex < TICK_DEFINITIONS.length; tickDefinitionIndex++) {
const curTickCount = difInstant / TICK_DEFINITIONS[tickDefinitionIndex].step;
let _tickDefIndex = 0;
for (; _tickDefIndex < TICK_DEFINITIONS.length; _tickDefIndex++) {
const curTickCount = difInstant / TICK_DEFINITIONS[_tickDefIndex].tick;
if (curTickCount < maxTickCount) {
// console.log('using definition', TICK_DEFINITIONS[tickDefinitionIndex].step / (1000 * 60 * 60));
break;
}
}

minInstant = getTickInstant(records[0].instant, TICK_DEFINITIONS[tickDefinitionIndex].step);
maxInstant = getTickInstant(records[records.length - 1].instant, TICK_DEFINITIONS[tickDefinitionIndex].step);
minInstant = getTickInstant(records[0].instant, TICK_DEFINITIONS[_tickDefIndex].tick);
maxInstant = getTickInstant(records[records.length - 1].instant, TICK_DEFINITIONS[_tickDefIndex].tick);

const _tickInterval: number[] = [];
for (let instant = minInstant; instant < maxInstant; instant += TICK_DEFINITIONS[tickDefinitionIndex].step) {
for (let instant = minInstant; instant < maxInstant; instant += TICK_DEFINITIONS[_tickDefIndex].tick) {
_tickInterval.push(instant);
}
setTickDefIndex(_tickDefIndex);
setTickInterval(_tickInterval);

}
Expand All @@ -121,6 +125,59 @@ const ChartValues = (props: IChartProps) => {

}

const getTickInstant = (instant: number, step: number) => {
const offsetInstant = instant - TimeUtil.getTimezoneOffsetSeconds() * 1000;
const moduloInstant = offsetInstant - offsetInstant % step + step
return moduloInstant + TimeUtil.getTimezoneOffsetSeconds() * 1000;
}

const rebuildStepRecords = () => {

if (records?.length > 0) {

const step = TICK_DEFINITIONS[tickDefIndex].tick / TICK_DEFINITIONS[tickDefIndex].step;
console.log('step', step);

let minInstant = getTickInstant(records[0].instant - step, step);
let maxInstant = getTickInstant(records[records.length - 1].instant, step);

const _stepRecords: IRecord[] = [];
let record: IRecord;
let recordIndex = 0;
let offInstant: number;
for (let instant = minInstant; instant < maxInstant; instant += step) {
for (; recordIndex < records.length; recordIndex++) {
record = records[recordIndex];
offInstant = record.instant - instant; // positive value while records are older than instant
if (Math.abs(offInstant) <= TimeUtil.MILLISECONDS_PER_MINUTE / 2) {
_stepRecords.push(record);
// break; // will continue with the next searchable instant
} else if (offInstant > 0) {
break;
}

}
}
setStepRecords(_stepRecords);

}

// console.log(new Date(minInstant), new Date(maxInstant));

// if (records?.length > 0) {

// let lastInstant = records[0].instant;
// for (let record of records) {
// if (record.instant - lastInstant >= TICK_DEFINITIONS[tickDefIndex].step) {
// _stepRecords.push(record);
// lastInstant = record.instant;
// }
// }
// setStepRecords(_stepRecords);
// }

}

const handleRefChange = (ref: SVGElement) => {

console.debug(`⚙ updating chart component (ref)`, ref);
Expand All @@ -141,24 +198,30 @@ const ChartValues = (props: IChartProps) => {

useEffect(() => {

if (tickInterval && exportTo !== '') {
console.debug(`⚙ updating chart component (tickInterval, exportTo)`, tickInterval, exportTo);
if (exportTo === 'png') {
setTimeout(() => {
exportToPng();
}, 250);
console.debug(`⚙ updating chart component (tickInterval)`, tickInterval);
rebuildStepRecords();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tickInterval]);

useEffect(() => {

if (stepRecords?.length > 0) {
console.debug(`⚙ updating chart component (stepRecords, exportTo)`, stepRecords, exportTo);
if (exportTo !== '') {
if (exportTo === 'png') {
setTimeout(() => {
exportToPng();
}, 250);

}
}
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tickInterval]);
}, [stepRecords]);


const getTickInstant = (instant: number, step: number) => {
const offsetInstant = instant - TimeUtil.getTimezoneOffsetSeconds() * 1000;
const moduloInstant = offsetInstant - offsetInstant % step + step
return moduloInstant + TimeUtil.getTimezoneOffsetSeconds() * 1000;
}

useEffect(() => {

Expand Down Expand Up @@ -219,7 +282,7 @@ const ChartValues = (props: IChartProps) => {
curve: 'linear',
valueFormatter: seriesDef.valueFormatter
}]}
dataset={records}
dataset={stepRecords}
grid={{ vertical: true, horizontal: true }}
margin={{ top: 15, right: 10, bottom: 65, left: 60 }}
sx={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const ConfigInputNumber = (props: IValueNumberConfig) => {
numberValue = Math.min(max, numberValue);
}
numberValue = fixed ? Math.round(numberValue) : numberValue;
if (!Number.isFinite(numberValue)) {
numberValue = value;
}
if (numberValue !== value) {
handleUpdate(numberValue);
} else {
Expand Down
Loading

0 comments on commit 088b355

Please sign in to comment.