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 (
-
- );
- }
- };
-
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 (
+
+ );
+};
+
+export default SettingsCustomizationDialog;