dashboard v.01

This commit is contained in:
proddy
2024-10-03 18:06:46 +02:00
parent f9f87ddc0e
commit fb0d9454ef
7 changed files with 246 additions and 23 deletions

View File

@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import CustomEntities from 'app/main/CustomEntities'; import CustomEntities from 'app/main/CustomEntities';
import Customizations from 'app/main/Customizations'; import Customizations from 'app/main/Customizations';
import Dashboard from 'app/main/Dashboard';
import Devices from 'app/main/Devices'; import Devices from 'app/main/Devices';
import Help from 'app/main/Help'; import Help from 'app/main/Help';
import Modules from 'app/main/Modules'; import Modules from 'app/main/Modules';
@@ -32,6 +33,7 @@ const AuthenticatedRouting = () => {
return ( return (
<Layout> <Layout>
<Routes> <Routes>
<Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/devices/*" element={<Devices />} /> <Route path="/devices/*" element={<Devices />} />
<Route path="/sensors/*" element={<Sensors />} /> <Route path="/sensors/*" element={<Sensors />} />
<Route path="/status/*" element={<Status />} /> <Route path="/status/*" element={<Status />} />

View File

@@ -5,6 +5,7 @@ import type {
Action, Action,
Activity, Activity,
CoreData, CoreData,
DashboardData,
DeviceData, DeviceData,
DeviceEntity, DeviceEntity,
Entities, Entities,
@@ -19,7 +20,13 @@ import type {
WriteTemperatureSensor WriteTemperatureSensor
} from '../app/main/types'; } from '../app/main/types';
// DashboardDevices // Dashboard
export const readDashboard = () =>
alovaInstance.Get<DashboardData>('/rest/dashboardData', {
responseType: 'arraybuffer' // uses msgpack
});
// Devices
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`); export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
export const readDeviceData = (id: number) => export const readDeviceData = (id: number) =>
alovaInstance.Get<DeviceData>('/rest/deviceData', { alovaInstance.Get<DeviceData>('/rest/deviceData', {

View File

@@ -0,0 +1,193 @@
import { useEffect, useState } from 'react';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { CellTree, useTree } from '@table-library/react-table-library/tree';
import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { readDashboard } from '../../api/app';
import { formatValue } from './deviceValue';
import type { DashboardItem } from './types';
const Dashboard = () => {
const { LL } = useI18nContext();
useLayoutTitle('Dashboard'); // TODO translate
const [firstLoad, setFirstLoad] = useState<boolean>(true);
const [showAll, setShowAll] = useState<boolean>(true);
const {
data,
send: fetchDashboard,
error
} = useAutoRequest(readDashboard, {
initialData: [],
pollingTime: 1500
});
const dashboard_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px;
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
border-bottom: 1px solid #565656;
height: 36px;
}
`,
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
.td {
height: 24px;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
`
});
function onTreeChange(action, state) {
// do nothing for now
}
const tree = useTree(
{ nodes: data },
{
onChange: onTreeChange
},
{
treeIcon: {
margin: '4px',
iconDefault: null,
iconRight: <ChevronRightIcon color="primary" />,
iconDown: <ExpandMoreIcon color="primary" />
},
indentation: 28
}
);
// auto expand on first load
useEffect(() => {
if (data.length && firstLoad && !tree.state.ids.length) {
tree.fns.onToggleAll({});
setFirstLoad(false);
}
}, [data]);
const showName = (di: DashboardItem) => {
if (di.id < 100) {
if (di.nodes?.length) {
return (
<div style={{ color: '#2196f3' }}>
{di.n} ({di.nodes?.length})
</div>
);
}
return <div style={{ color: '#2196f3' }}>{di.n}</div>;
}
return <div>{di.n}</div>;
};
const handleShowAll = (
event: React.MouseEvent<HTMLElement>,
toggle: boolean | null
) => {
if (toggle !== null) {
tree.fns.onToggleAll({});
setShowAll(toggle);
}
};
const renderContent = () => {
if (!data) {
return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />;
}
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">
Use Customizations to mark your favorite EMS entities
</Typography>
</Box>
<ToggleButtonGroup
color="primary"
size="small"
value={showAll}
exclusive
onChange={handleShowAll}
>
<ToggleButton value={true}>
<UnfoldMoreIcon fontSize="small" />
</ToggleButton>
<ToggleButton value={false}>
<UnfoldLessIcon fontSize="small" />
</ToggleButton>
</ToggleButtonGroup>
<Table
data={{ nodes: data }}
theme={dashboard_theme}
layout={{ custom: true }}
tree={tree}
>
{(tableList: DashboardItem[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize>Name</HeaderCell>
<HeaderCell>Value</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((di: DashboardItem) => (
<Row key={di.id} item={di} disabled={di.nodes?.length === 0}>
{di.nodes?.length === 0 ? (
<Cell>{showName(di)}</Cell>
) : (
<CellTree item={di}>{showName(di)}</CellTree>
)}
<Cell pinRight>{formatValue(LL, di.v, di.u)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
</>
);
};
return <SectionContent>{renderContent()}</SectionContent>;
};
export default Dashboard;

View File

@@ -114,6 +114,18 @@ export interface CoreData {
devices: Device[]; devices: Device[];
} }
export interface DashboardItem {
id: number; // unique index
n: string; // name
v?: unknown; // value
u: number; // uom
nodes?: DashboardItem[]; // nodes
}
export interface DashboardData {
data: DashboardItem[];
}
export interface DeviceValue { export interface DeviceValue {
id: string; // index, contains mask+name id: string; // index, contains mask+name
v: unknown; // value, Number or String v: unknown; // value, Number or String
@@ -125,6 +137,7 @@ export interface DeviceValue {
m?: number; // min, optional m?: number; // min, optional
x?: number; // max, optional x?: number; // max, optional
} }
export interface DeviceData { export interface DeviceData {
data: DeviceValue[]; data: DeviceValue[];
} }

View File

@@ -11,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import SensorsIcon from '@mui/icons-material/Sensors'; import SensorsIcon from '@mui/icons-material/Sensors';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import StarIcon from '@mui/icons-material/Star';
import { import {
Avatar, Avatar,
Box, Box,
@@ -51,6 +52,7 @@ const LayoutMenu = () => {
return ( return (
<> <>
<List component="nav"> <List component="nav">
<LayoutMenuItem icon={StarIcon} label="Dashboard" to={`/dashboard`} />
<LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} /> <LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} />
<LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} /> <LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} />
<Divider /> <Divider />

View File

@@ -35,7 +35,7 @@ export function fetchLoginRedirect(): Partial<Path> {
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH); const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
clearLoginRedirect(); clearLoginRedirect();
return { return {
pathname: signInPathname || `/devices`, pathname: signInPathname || `/dashboard`,
search: (signInPathname && signInSearch) || undefined search: (signInPathname && signInSearch) || undefined
}; };
} }

View File

@@ -4277,7 +4277,8 @@ function getDashboardEntityData(id: number) {
id2: id id2: id
})) }))
.filter((item) => id === 99 || parseInt(item.id2.slice(0, 2), 16) & 0x08) .filter((item) => id === 99 || parseInt(item.id2.slice(0, 2), 16) & 0x08)
.map((item) => ({ .map((item, index) => ({
id: id * 100 + index, // unique id
n: item.id2.slice(2), // name n: item.id2.slice(2), // name
v: item.v, // value v: item.v, // value
u: item.u // uom u: item.u // uom
@@ -4324,7 +4325,6 @@ router
params.id ? deviceEntities(Number(params.id)) : status(404) params.id ? deviceEntities(Number(params.id)) : status(404)
) )
.get(EMSESP_DASHBOARD_DATA_ENDPOINT, () => { .get(EMSESP_DASHBOARD_DATA_ENDPOINT, () => {
// builds a JSON with id, t = typeID, tn = typeName, n=Name, data = [{n, v, u}]
let dashboard_data = []; let dashboard_data = [];
let dashboard_object = {}; let dashboard_object = {};
@@ -4334,57 +4334,63 @@ router
dashboard_object = { dashboard_object = {
id: id, id: id,
t: element.t,
tn: element.tn,
n: element.n, n: element.n,
data: getDashboardEntityData(id) nodes: getDashboardEntityData(id)
}; };
dashboard_data.push(dashboard_object); // add to dashboard_data // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) {
dashboard_data.push(dashboard_object);
}
} }
// add the custom entity data // add the custom entity data
dashboard_object = { dashboard_object = {
id: 99, id: 99,
t: 99,
tn: 'custom',
n: 'Custom Entities', n: 'Custom Entities',
data: getDashboardEntityData(99) nodes: getDashboardEntityData(99)
}; };
dashboard_data.push(dashboard_object); // add to dashboard_data // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) {
dashboard_data.push(dashboard_object);
}
// add temperature sensor data // add temperature sensor data
let sensor_data = {}; let sensor_data = {};
sensor_data = emsesp_sensordata.ts.map((item) => ({ sensor_data = emsesp_sensordata.ts.map((item, index) => ({
id: 980 + index,
n: item.n ? item.n : item.id, // name may not be set n: item.n ? item.n : item.id, // name may not be set
v: item.t ? item.t : undefined, // can have no value v: item.t ? item.t : undefined, // can have no value
u: item.u u: item.u
})); }));
dashboard_object = { dashboard_object = {
id: 98, id: 98,
t: 98,
tn: 'ts',
n: 'Temperature Sensors', n: 'Temperature Sensors',
data: sensor_data nodes: sensor_data
}; };
dashboard_data.push(dashboard_object); // add to dashboard_data // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) {
dashboard_data.push(dashboard_object);
}
// add analog sensor data // add analog sensor data
sensor_data = emsesp_sensordata.as.map((item) => ({ sensor_data = emsesp_sensordata.as.map((item, index) => ({
id: 970 + index,
n: item.n, n: item.n,
v: item.v, v: item.v,
u: item.u u: item.u
})); }));
dashboard_object = { dashboard_object = {
id: 97, id: 97,
t: 97,
tn: 'as',
n: 'Analog Sensors', n: 'Analog Sensors',
data: sensor_data nodes: sensor_data
}; };
dashboard_data.push(dashboard_object); // add to dashboard_data // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) {
dashboard_data.push(dashboard_object);
}
console.log('dashboard_data: ', dashboard_data); // console.log('dashboard_data: ', dashboard_data);
return new Response(encoder.encode(dashboard_data), { headers }); // msgpack it return new Response(encoder.encode(dashboard_data), { headers }); // msgpack it
}) })