add web page for modules

This commit is contained in:
proddy
2024-05-29 22:23:01 +02:00
parent 3fa42be6d4
commit fb6b8813c7
8 changed files with 625 additions and 92 deletions

View File

@@ -16,6 +16,7 @@ import ApplicationSettings from 'project/ApplicationSettings';
import CustomEntities from 'project/CustomEntities';
import Customization from 'project/Customization';
import Devices from 'project/Devices';
import Modules from 'project/Modules';
import Scheduler from 'project/Scheduler';
import Sensors from 'project/Sensors';
@@ -43,6 +44,7 @@ const AuthenticatedRouting: FC = () => {
<Route path="/settings/ntp/*" element={<NetworkTime />} />
<Route path="/settings/mqtt/*" element={<Mqtt />} />
<Route path="/settings/security/*" element={<Security />} />
<Route path="/settings/modules/*" element={<Modules />} />
<Route path="/system/espsystemstatus/*" element={<ESPSystemStatus />} />
<Route path="/settings/upload/*" element={<UploadDownload />} />
</>

View File

@@ -9,6 +9,7 @@ import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import {
Box,
Button,
@@ -122,6 +123,14 @@ const Settings: FC = () => {
to="security"
/>
<ListMenuItem
icon={ViewModuleIcon}
bgcolor="#efc34b"
label="Modules"
text="Activate or deactivate external modules"
to="modules"
/>
<Divider />
<ListMenuItem

View File

@@ -0,0 +1,228 @@
import { useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import CircleIcon from '@mui/icons-material/Circle';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, 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 { updateState, useRequest } from 'alova';
import {
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import type { ModuleItem, Modules } from './types';
const Modules: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const {
data: modules,
send: fetchModules,
error
} = useRequest(EMSESP.readModules, {
initialData: [],
force: true
});
const { send: writeModules } = useRequest(
(data: Modules) => EMSESP.writeModules(data),
{
immediate: false
}
);
const modules_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr));
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
text-align: center;
}
`,
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 {
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
`
});
const onCancel = async () => {
await fetchModules().then(() => {
setNumChanges(0);
});
};
function hasModulesChanged(mi: ModuleItem) {
return mi.enabled !== mi.o_enabled;
}
const selectModule = (updatedItem: ModuleItem) => {
updatedItem.enabled = !updatedItem.enabled;
updateState('modules', (data: ModuleItem[]) => {
const new_data = data.map((mi) =>
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
);
setNumChanges(new_data.filter((mi) => hasModulesChanged(mi)).length);
return new_data;
});
};
const saveModules = async () => {
await writeModules({
modules: modules.map((condensed_mi) => ({
name: condensed_mi.name,
enabled: condensed_mi.enabled
}))
})
.then(() => {
toast.success('Modules saved');
})
.catch((error: Error) => {
toast.error(error.message);
})
.finally(async () => {
await fetchModules();
setNumChanges(0);
});
};
const renderModules = () => {
if (!modules) {
return <FormLoader onRetry={fetchModules} errorMessage={error?.message} />;
}
useLayoutTitle('Modules');
return (
<Table
data={{ nodes: modules }}
theme={modules_theme}
layout={{ custom: true }}
>
{(tableList: ModuleItem[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell />
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
<HeaderCell stiff>Author</HeaderCell>
<HeaderCell stiff>{LL.VERSION()}</HeaderCell>
<HeaderCell stiff>{LL.STATUS_OF('')}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((mi: ModuleItem) => (
<Row key={mi.id} item={mi} onClick={() => selectModule(mi)}>
<Cell stiff>
{mi.enabled ? (
<CircleIcon
color="success"
sx={{ fontSize: 16, verticalAlign: 'middle' }}
/>
) : (
<CircleIcon
color="error"
sx={{ fontSize: 16, verticalAlign: 'middle' }}
/>
)}
</Cell>
<Cell>{mi.name}</Cell>
<Cell>{mi.author}</Cell>
<Cell>{mi.version}</Cell>
<Cell>{mi.status}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">
Activate or de-activate EMS-ESP library modules (** experimental **)
</Typography>
</Box>
{renderModules()}
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onCancel}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
color="info"
onClick={saveModules}
>
{LL.APPLY_CHANGES(numChanges)}
</Button>
</ButtonRow>
)}
</Box>
</Box>
</SectionContent>
);
};
export default Modules;

View File

@@ -9,6 +9,8 @@ import type {
Devices,
Entities,
EntityItem,
ModuleItem,
Modules,
Schedule,
ScheduleItem,
SensorData,
@@ -104,6 +106,20 @@ export const readSchedule = () =>
export const writeSchedule = (data: Schedule) =>
alovaInstance.Post('/rest/schedule', data);
// Modules
export const readModules = () =>
alovaInstance.Get<ModuleItem[]>('/rest/modules', {
name: 'modules',
transformData(data) {
return (data as Modules).modules.map((mi: ModuleItem) => ({
...mi,
o_enabled: mi.enabled
}));
}
});
export const writeModules = (data: Modules) =>
alovaInstance.Post('/rest/modules', data);
// SettingsEntities
export const readCustomEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/customEntities', {

View File

@@ -308,6 +308,20 @@ export interface Schedule {
schedule: ScheduleItem[];
}
export interface ModuleItem {
id: number; // unique index
name: string;
author: string;
version: string;
status: string;
enabled: boolean;
o_enabled?: boolean;
}
export interface Modules {
modules: ModuleItem[];
}
export enum ScheduleFlag {
SCHEDULE_SUN = 1,
SCHEDULE_MON = 2,