diff --git a/interface/src/AuthenticatedRouting.tsx b/interface/src/AuthenticatedRouting.tsx index 3942e428b..adf39beb1 100644 --- a/interface/src/AuthenticatedRouting.tsx +++ b/interface/src/AuthenticatedRouting.tsx @@ -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 ( + } /> } /> } /> } /> diff --git a/interface/src/api/app.ts b/interface/src/api/app.ts index a856d2096..a56f4db53 100644 --- a/interface/src/api/app.ts +++ b/interface/src/api/app.ts @@ -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('/rest/dashboardData', { + responseType: 'arraybuffer' // uses msgpack + }); + +// Devices export const readCoreData = () => alovaInstance.Get(`/rest/coreData`); export const readDeviceData = (id: number) => alovaInstance.Get('/rest/deviceData', { diff --git a/interface/src/app/main/Dashboard.tsx b/interface/src/app/main/Dashboard.tsx new file mode 100644 index 000000000..cc9dff0bf --- /dev/null +++ b/interface/src/app/main/Dashboard.tsx @@ -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(true); + const [showAll, setShowAll] = useState(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: , + iconDown: + }, + 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 ( +
+ {di.n} ({di.nodes?.length}) +
+ ); + } + return
{di.n}
; + } + + return
{di.n}
; + }; + + const handleShowAll = ( + event: React.MouseEvent, + toggle: boolean | null + ) => { + if (toggle !== null) { + tree.fns.onToggleAll({}); + setShowAll(toggle); + } + }; + + const renderContent = () => { + if (!data) { + return ; + } + + return ( + <> + + + Use Customizations to mark your favorite EMS entities + + + + + + + + + + + + {(tableList: DashboardItem[]) => ( + <> +
+ + Name + Value + +
+ + {tableList.map((di: DashboardItem) => ( + + {di.nodes?.length === 0 ? ( + {showName(di)} + ) : ( + {showName(di)} + )} + {formatValue(LL, di.v, di.u)} + + ))} + + + )} +
+ + ); + }; + + return {renderContent()}; +}; + +export default Dashboard; diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 3681f8e09..30efb7e59 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -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[]; } diff --git a/interface/src/components/layout/LayoutMenu.tsx b/interface/src/components/layout/LayoutMenu.tsx index 4b9051d03..1817abc2e 100644 --- a/interface/src/components/layout/LayoutMenu.tsx +++ b/interface/src/components/layout/LayoutMenu.tsx @@ -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 ( <> + diff --git a/interface/src/components/routing/authentication.ts b/interface/src/components/routing/authentication.ts index 822417b88..ae93fa88e 100644 --- a/interface/src/components/routing/authentication.ts +++ b/interface/src/components/routing/authentication.ts @@ -35,7 +35,7 @@ export function fetchLoginRedirect(): Partial { const signInSearch = getStorage().getItem(SIGN_IN_SEARCH); clearLoginRedirect(); return { - pathname: signInPathname || `/devices`, + pathname: signInPathname || `/dashboard`, search: (signInPathname && signInSearch) || undefined }; } diff --git a/mock-api/rest_server.ts b/mock-api/rest_server.ts index 7a915008f..b57af9db4 100644 --- a/mock-api/rest_server.ts +++ b/mock-api/rest_server.ts @@ -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 })