Files
EMS-ESP32/interface/src/project/SettingsCustomEntities.tsx
2024-01-21 11:45:40 +01:00

302 lines
9.1 KiB
TypeScript

import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import RefreshIcon from '@mui/icons-material/Refresh';
import WarningIcon from '@mui/icons-material/Warning';
import { Button, Typography, Box } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useState, useCallback } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import SettingsCustomEntitiesDialog from './SettingsCustomEntitiesDialog';
import * as EMSESP from './api';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import { entityItemValidation } from './validators';
import type { EntityItem } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const SettingsCustomEntities: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const {
data: entities,
send: fetchEntities,
error
} = useRequest(EMSESP.readCustomEntities, {
initialData: [],
force: true
});
const { send: writeEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false });
function hasEntityChanged(ei: EntityItem) {
return (
ei.id !== ei.o_id ||
ei.ram !== ei.o_ram ||
(ei?.name || '') !== (ei?.o_name || '') ||
ei.device_id !== ei.o_device_id ||
ei.type_id !== ei.o_type_id ||
ei.offset !== ei.o_offset ||
ei.uom !== ei.o_uom ||
ei.factor !== ei.o_factor ||
ei.value_type !== ei.o_value_type ||
ei.writeable !== ei.o_writeable ||
ei.deleted !== ei.o_deleted ||
(ei.value || '') !== (ei.o_value || '')
);
}
const entity_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 90px;
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
padding: 8px;
}
&:nth-of-type(2) {
text-align: center;
}
&:nth-of-type(3) {
text-align: center;
}
&:nth-of-type(4) {
text-align: center;
}
&:nth-of-type(5) {
text-align: center;
}
&:nth-of-type(6) {
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 saveEntities = async () => {
await writeEntities({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
id: condensed_ei.id,
ram: condensed_ei.ram,
name: condensed_ei.name,
device_id: condensed_ei.device_id,
type_id: condensed_ei.type_id,
offset: condensed_ei.offset,
factor: condensed_ei.factor,
uom: condensed_ei.uom,
writeable: condensed_ei.writeable,
value_type: condensed_ei.value_type,
value: condensed_ei.value
}))
})
.then(() => {
toast.success(LL.ENTITIES_UPDATED());
})
.catch((err) => {
toast.error(err.message);
})
.finally(async () => {
await fetchEntities();
setNumChanges(0);
});
};
const editEntityItem = useCallback((ei: EntityItem) => {
setCreating(false);
setSelectedEntityItem(ei);
setDialogOpen(true);
}, []);
const onDialogClose = () => {
setDialogOpen(false);
};
const onDialogCancel = async () => {
await fetchEntities().then(() => {
setNumChanges(0);
});
};
const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false);
updateState('entities', (data) => {
const new_data = creating
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data;
});
};
const addEntityItem = () => {
setCreating(true);
setSelectedEntityItem({
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
name: '',
ram: 0,
device_id: '0',
type_id: '0',
offset: 0,
factor: 1,
uom: 0,
value_type: 0,
writeable: false,
deleted: false,
value: ''
});
setDialogOpen(true);
};
function formatValue(value: any, uom: number) {
return value === undefined || uom === undefined
? ''
: typeof value === 'number'
? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
: value;
}
function showHex(value: number, digit: number) {
return '0x' + value.toString(16).toUpperCase().padStart(digit, '0');
}
const renderEntity = () => {
if (!entities) {
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
}
return (
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell>{LL.NAME(0)}</HeaderCell>
<HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
<HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
<HeaderCell stiff>{LL.OFFSET()}</HeaderCell>
<HeaderCell stiff>{LL.TYPE(1)}</HeaderCell>
<HeaderCell stiff>{LL.VALUE(1)}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((ei: EntityItem) => (
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>
{ei.name}&nbsp;
{ei.writeable && <EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
</Cell>
<Cell>{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}</Cell>
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
<Cell>{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
);
};
return (
<SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
</Box>
{renderEntity()}
{selectedEntityItem && (
<SettingsCustomEntitiesDialog
open={dialogOpen}
creating={creating}
onClose={onDialogClose}
onSave={onDialogSave}
selectedItem={selectedEntityItem}
validator={entityItemValidation()}
/>
)}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
color="info"
onClick={saveEntities}
>
{LL.APPLY_CHANGES(numChanges)}
</Button>
</ButtonRow>
)}
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchEntities}>
{LL.REFRESH()}
</Button>
<Button startIcon={<AddIcon />} variant="outlined" color="primary" onClick={addEntityItem}>
{LL.ADD(0)}
</Button>
</ButtonRow>
</Box>
</Box>
</SectionContent>
);
};
export default SettingsCustomEntities;