mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
dashboard v.01
This commit is contained in:
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import CustomEntities from 'app/main/CustomEntities';
|
||||
import Customizations from 'app/main/Customizations';
|
||||
import Dashboard from 'app/main/Dashboard';
|
||||
import Devices from 'app/main/Devices';
|
||||
import Help from 'app/main/Help';
|
||||
import Modules from 'app/main/Modules';
|
||||
@@ -32,6 +33,7 @@ const AuthenticatedRouting = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route path="/devices/*" element={<Devices />} />
|
||||
<Route path="/sensors/*" element={<Sensors />} />
|
||||
<Route path="/status/*" element={<Status />} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
Action,
|
||||
Activity,
|
||||
CoreData,
|
||||
DashboardData,
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
Entities,
|
||||
@@ -19,7 +20,13 @@ import type {
|
||||
WriteTemperatureSensor
|
||||
} 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 readDeviceData = (id: number) =>
|
||||
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
||||
|
||||
193
interface/src/app/main/Dashboard.tsx
Normal file
193
interface/src/app/main/Dashboard.tsx
Normal 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;
|
||||
@@ -114,6 +114,18 @@ export interface CoreData {
|
||||
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 {
|
||||
id: string; // index, contains mask+name
|
||||
v: unknown; // value, Number or String
|
||||
@@ -125,6 +137,7 @@ export interface DeviceValue {
|
||||
m?: number; // min, optional
|
||||
x?: number; // max, optional
|
||||
}
|
||||
|
||||
export interface DeviceData {
|
||||
data: DeviceValue[];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import SensorsIcon from '@mui/icons-material/Sensors';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -51,6 +52,7 @@ const LayoutMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<List component="nav">
|
||||
<LayoutMenuItem icon={StarIcon} label="Dashboard" to={`/dashboard`} />
|
||||
<LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} />
|
||||
<LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} />
|
||||
<Divider />
|
||||
|
||||
@@ -35,7 +35,7 @@ export function fetchLoginRedirect(): Partial<Path> {
|
||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
return {
|
||||
pathname: signInPathname || `/devices`,
|
||||
pathname: signInPathname || `/dashboard`,
|
||||
search: (signInPathname && signInSearch) || undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4277,7 +4277,8 @@ function getDashboardEntityData(id: number) {
|
||||
id2: id
|
||||
}))
|
||||
.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
|
||||
v: item.v, // value
|
||||
u: item.u // uom
|
||||
@@ -4324,7 +4325,6 @@ router
|
||||
params.id ? deviceEntities(Number(params.id)) : status(404)
|
||||
)
|
||||
.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_object = {};
|
||||
|
||||
@@ -4334,57 +4334,63 @@ router
|
||||
|
||||
dashboard_object = {
|
||||
id: id,
|
||||
t: element.t,
|
||||
tn: element.tn,
|
||||
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
|
||||
dashboard_object = {
|
||||
id: 99,
|
||||
t: 99,
|
||||
tn: 'custom',
|
||||
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
|
||||
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
|
||||
v: item.t ? item.t : undefined, // can have no value
|
||||
u: item.u
|
||||
}));
|
||||
dashboard_object = {
|
||||
id: 98,
|
||||
t: 98,
|
||||
tn: 'ts',
|
||||
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
|
||||
sensor_data = emsesp_sensordata.as.map((item) => ({
|
||||
sensor_data = emsesp_sensordata.as.map((item, index) => ({
|
||||
id: 970 + index,
|
||||
n: item.n,
|
||||
v: item.v,
|
||||
u: item.u
|
||||
}));
|
||||
dashboard_object = {
|
||||
id: 97,
|
||||
t: 97,
|
||||
tn: 'as',
|
||||
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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user