diff --git a/interface/src/project/EntityMaskToggle.tsx b/interface/src/project/EntityMaskToggle.tsx new file mode 100644 index 000000000..f02af3cea --- /dev/null +++ b/interface/src/project/EntityMaskToggle.tsx @@ -0,0 +1,81 @@ +import { ToggleButton, ToggleButtonGroup } from '@mui/material'; +import OptionIcon from './OptionIcon'; +import { DeviceEntityMask } from './types'; +import type { DeviceEntity } from './types'; + +type EntityMaskToggleProps = { + onUpdate: (de: DeviceEntity) => void; + de: DeviceEntity; +}; + +const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => { + const getMaskNumber = (newMask: string[]) => { + let new_mask = 0; + for (const entry of newMask) { + new_mask |= Number(entry); + } + return new_mask; + }; + + const getMaskString = (m: number) => { + const new_masks: string[] = []; + if ((m & 1) === 1) { + new_masks.push('1'); + } + if ((m & 2) === 2) { + new_masks.push('2'); + } + if ((m & 4) === 4) { + new_masks.push('4'); + } + if ((m & 8) === 8) { + new_masks.push('8'); + } + if ((m & 128) === 128) { + new_masks.push('128'); + } + return new_masks; + }; + + return ( + { + de.m = getMaskNumber(mask); + if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { + de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; + } + if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { + de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; + } + onUpdate(de); + }} + > + + + + = 3}> + + + + + + + + + + + + + ); +}; + +export default EntityMaskToggle; diff --git a/interface/src/project/OptionIcon.tsx b/interface/src/project/OptionIcon.tsx index d1cbdd2b7..f05f4c3c9 100644 --- a/interface/src/project/OptionIcon.tsx +++ b/interface/src/project/OptionIcon.tsx @@ -32,9 +32,9 @@ interface OptionIconProps { const OptionIcon: FC = ({ type, isSet }) => { const Icon = OPTION_ICONS[type][isSet ? 0 : 1]; return isSet ? ( - + ) : ( - + ); }; diff --git a/interface/src/project/SettingsCustomization.tsx b/interface/src/project/SettingsCustomization.tsx index e44832c17..8d142a0a0 100644 --- a/interface/src/project/SettingsCustomization.tsx +++ b/interface/src/project/SettingsCustomization.tsx @@ -1,7 +1,4 @@ import CancelIcon from '@mui/icons-material/Cancel'; -import DoneIcon from '@mui/icons-material/Done'; - -import FilterListIcon from '@mui/icons-material/FilterList'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import SearchIcon from '@mui/icons-material/Search'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; @@ -17,10 +14,10 @@ import { DialogTitle, ToggleButton, ToggleButtonGroup, - Tooltip, Grid, TextField, - Link + Link, + InputAdornment } 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'; @@ -28,71 +25,38 @@ import { useState, useEffect, useCallback } from 'react'; import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { toast } from 'react-toastify'; +import EntityMaskToggle from './EntityMaskToggle'; import OptionIcon from './OptionIcon'; +import SettingsCustomizationDialog from './SettingsCustomizationDialog'; import * as EMSESP from './api'; import { DeviceEntityMask } from './types'; import type { DeviceShort, Devices, DeviceEntity } from './types'; import type { FC } from 'react'; -import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox, BlockNavigation } from 'components'; +import { ButtonRow, FormLoader, SectionContent, MessageBox, BlockNavigation } from 'components'; import RestartMonitor from 'framework/system/RestartMonitor'; import { useI18nContext } from 'i18n/i18n-react'; -import { extractErrorMessage, updateValue } from 'utils'; +import { extractErrorMessage } from 'utils'; export const APIURL = window.location.origin + '/api/'; const SettingsCustomization: FC = () => { const { LL } = useI18nContext(); - - const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false }; - const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); - const [restarting, setRestarting] = useState(false); const [restartNeeded, setRestartNeeded] = useState(false); - const [deviceEntities, setDeviceEntities] = useState([emptyDeviceEntity]); + const [deviceEntities, setDeviceEntities] = useState(); const [devices, setDevices] = useState(); const [errorMessage, setErrorMessage] = useState(); const [selectedDevice, setSelectedDevice] = useState(-1); const [confirmReset, setConfirmReset] = useState(false); const [selectedFilters, setSelectedFilters] = useState(0); const [search, setSearch] = useState(''); - const [deviceEntity, setDeviceEntity] = useState(); - // eslint-disable-next-line - const [masks, setMasks] = useState(() => ['']); - - function hasEntityChanged(de: DeviceEntity) { - return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi; - } - - const getChanges = () => { - if (!deviceEntities || selectedDevice === -1) { - return []; - } - - return deviceEntities - .filter((de) => hasEntityChanged(de)) - .map( - (new_de) => - new_de.m.toString(16).padStart(2, '0') + - new_de.id + - (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + - (new_de.cn ? new_de.cn : '') + - (new_de.mi ? '>' + new_de.mi : '') + - (new_de.ma ? '<' + new_de.ma : '') - ); - }; - - const countChanges = () => { - setNumChanges(getChanges().length); - }; - - useEffect(() => { - countChanges(); - }); + const [selectedDeviceEntity, setSelectedDeviceEntity] = useState(); + const [dialogOpen, setDialogOpen] = useState(false); const entities_theme = useTheme({ Table: ` @@ -119,12 +83,10 @@ const SettingsCustomization: FC = () => { text-transform: uppercase; background-color: black; color: #90CAF9; - .th { border-bottom: 1px solid #565656; height: 36px; } - &:nth-of-type(1) .th { text-align: center; } @@ -133,21 +95,17 @@ const SettingsCustomization: FC = () => { background-color: #1e1e1e; position: relative; cursor: pointer; - .td { border-top: 1px solid #565656; border-bottom: 1px solid #565656; } - &.tr.tr-body.row-select.row-select-single-selected { background-color: #3d4752; } - &:hover .td { border-top: 1px solid #177ac9; border-bottom: 1px solid #177ac9; } - &:nth-of-type(odd) .td { background-color: #303030; } @@ -168,6 +126,28 @@ const SettingsCustomization: FC = () => { ` }); + function hasEntityChanged(de: DeviceEntity) { + return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi; + } + + useEffect(() => { + if (deviceEntities) { + setNumChanges( + deviceEntities + .filter((de) => hasEntityChanged(de)) + .map( + (new_de) => + new_de.m.toString(16).padStart(2, '0') + + new_de.id + + (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + + (new_de.cn ? new_de.cn : '') + + (new_de.mi ? '>' + new_de.mi : '') + + (new_de.ma ? '<' + new_de.ma : '') + ).length + ); + } + }, [deviceEntities]); + const fetchDevices = useCallback(async () => { try { setDevices((await EMSESP.readDevices()).data); @@ -176,6 +156,11 @@ const SettingsCustomization: FC = () => { } }, [LL]); + // on mount + useEffect(() => { + void fetchDevices(); + }, [fetchDevices]); + const setOriginalSettings = (data: DeviceEntity[]) => { setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }))); }; @@ -189,10 +174,6 @@ const SettingsCustomization: FC = () => { } }; - useEffect(() => { - void fetchDevices(); - }, [fetchDevices]); - function formatValue(value: any) { if (typeof value === 'number') { return new Intl.NumberFormat().format(value); @@ -246,7 +227,7 @@ const SettingsCustomization: FC = () => { const maskDisabled = (set: boolean) => { setDeviceEntities( - deviceEntities.map(function (de) { + deviceEntities?.map(function (de) { if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) { return { ...de, @@ -291,9 +272,45 @@ const SettingsCustomization: FC = () => { } }; + const onDialogClose = () => { + setDialogOpen(false); + }; + + const updateDeviceEntity = (updatedItem: DeviceEntity) => { + setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de))); + }; + + const onDialogSave = (updatedItem: DeviceEntity) => { + setDialogOpen(false); + updateDeviceEntity(updatedItem); + }; + + const editDeviceEntity = useCallback((de: DeviceEntity) => { + if (de.n === undefined || (de.n && de.n[0] === '!')) { + return; + } + + if (de.cn === undefined) { + de.cn = ''; + } + + setSelectedDeviceEntity(de); + setDialogOpen(true); + }, []); + const saveCustomization = async () => { if (devices && deviceEntities && selectedDevice !== -1) { - const masked_entities = getChanges(); + const masked_entities = deviceEntities + .filter((de) => hasEntityChanged(de)) + .map( + (new_de) => + new_de.m.toString(16).padStart(2, '0') + + new_de.id + + (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + + (new_de.cn ? new_de.cn : '') + + (new_de.mi ? '>' + new_de.mi : '') + + (new_de.ma ? '<' + new_de.ma : '') + ); // check size in bytes to match buffer in CPP, which is 2048 const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length; @@ -329,8 +346,8 @@ const SettingsCustomization: FC = () => { return ( <> - {LL.CUSTOMIZATIONS_HELP_1()} - + {LL.CUSTOMIZATIONS_HELP_1()}. + ={LL.CUSTOMIZATIONS_HELP_2()}   ={LL.CUSTOMIZATIONS_HELP_3()}   ={LL.CUSTOMIZATIONS_HELP_4()}   @@ -338,7 +355,7 @@ const SettingsCustomization: FC = () => { ={LL.CUSTOMIZATIONS_HELP_6()} - { {device.s} ))} - + ); }; - const editEntity = (de: DeviceEntity) => { - if (de.n === undefined || (de.n && de.n[0] === '!')) { + const renderDeviceData = () => { + if (!deviceEntities) { return; } - if (de.cn === undefined) { - de.cn = ''; - } - setDeviceEntity(de); - }; - - const updateEntity = () => { - if (deviceEntity) { - setDeviceEntities((prevState) => { - const newState = prevState.map((obj) => { - if (obj.id === deviceEntity.id) { - return { ...obj, cn: deviceEntity.cn, mi: deviceEntity.mi, ma: deviceEntity.ma }; - } - return obj; - }); - return newState; - }); - } - - setDeviceEntity(undefined); - }; - - const renderDeviceData = () => { if (devices?.devices.length === 0 || deviceEntities[0].id === '') { return; } @@ -401,33 +395,23 @@ const SettingsCustomization: FC = () => { return ( <> - - - #: - - - - - {shown_data.length}/{deviceEntities.length} - - - - : - { setSearch(event.target.value); }} + InputProps={{ + startAdornment: ( + + + + ) + }} /> - - - : - - { - + + + {LL.SHOWING()} {shown_data.length}/{deviceEntities.length} + + {(tableList: any) => ( @@ -496,63 +484,14 @@ const SettingsCustomization: FC = () => { {tableList.map((de: DeviceEntity) => ( - editEntity(de)}> + editDeviceEntity(de)}> - {!deviceEntity && ( - { - de.m = getMaskNumber(mask); - if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { - de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; - } - if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { - de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; - } - setMasks(['']); // forces a refresh - }} - > - - - - = 3}> - - - - - - - - - - - - - )} + - {!deviceEntity && formatName(de)} - {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} - {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} - {!deviceEntity && formatValue(de.v)} + {formatName(de)} + {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} + {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} + {formatValue(de.v)} ))} @@ -632,83 +571,18 @@ const SettingsCustomization: FC = () => { ); - const renderEditDialog = () => { - if (deviceEntity) { - return ( - setDeviceEntity(undefined)}> - {LL.EDIT() + ' ' + LL.ENTITY() + ' "' + deviceEntity.id + '"'} - - - - {LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {deviceEntity.n} - - - - - - - {typeof deviceEntity.v === 'number' && - deviceEntity.w && - !(deviceEntity.m & DeviceEntityMask.DV_READONLY) && ( - <> - - - - - - - - )} - - - - - - - - ); - } - }; - return ( {blocker ? : null} {restarting ? : renderContent()} - {renderEditDialog()} + {selectedDeviceEntity && ( + + )} ); }; diff --git a/interface/src/project/SettingsCustomizationDialog.tsx b/interface/src/project/SettingsCustomizationDialog.tsx new file mode 100644 index 000000000..faa1e10a3 --- /dev/null +++ b/interface/src/project/SettingsCustomizationDialog.tsx @@ -0,0 +1,141 @@ +import CancelIcon from '@mui/icons-material/Cancel'; +import DoneIcon from '@mui/icons-material/Done'; + +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + TextField, + Typography +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +import EntityMaskToggle from './EntityMaskToggle'; +import { DeviceEntityMask } from './types'; +import type { DeviceEntity } from './types'; + +import { useI18nContext } from 'i18n/i18n-react'; + +import { updateValue } from 'utils'; + +type SettingsCustomizationDialogProps = { + open: boolean; + onClose: () => void; + onSave: (di: DeviceEntity) => void; + selectedDeviceEntity: DeviceEntity; +}; + +const SettingsCustomizationDialog = ({ + open, + onClose, + onSave, + selectedDeviceEntity +}: SettingsCustomizationDialogProps) => { + const { LL } = useI18nContext(); + const [editItem, setEditItem] = useState(selectedDeviceEntity); + const [error, setError] = useState(false); + + const updateFormValue = updateValue(setEditItem); + + const isWriteableNumber = + typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY); + + useEffect(() => { + if (open) { + setError(false); + setEditItem(selectedDeviceEntity); + } + }, [open, selectedDeviceEntity]); + + const close = () => { + onClose(); + }; + + const save = () => { + if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) { + setError(true); + } else { + onSave(editItem); + } + }; + + const updateDeviceEntity = (updatedItem: DeviceEntity) => { + setEditItem({ ...editItem, m: updatedItem.m }); + }; + + return ( + + {LL.EDIT() + ' ' + LL.ENTITY()} + + + + {LL.ENTITY()}: {editItem.id} + + + + + {LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {editItem.n} + + + + + + + + + + {isWriteableNumber && ( + <> + + + + + + + + )} + + {error && ( + + Error: Check min and max values + + )} + + + + + + + ); +}; + +export default SettingsCustomizationDialog;