mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-01-29 18:19:09 +03:00
optimize Settings Customization for rendering
This commit is contained in:
81
interface/src/project/EntityMaskToggle.tsx
Normal file
81
interface/src/project/EntityMaskToggle.tsx
Normal file
@@ -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 (
|
||||||
|
<ToggleButtonGroup
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
value={getMaskString(de.m)}
|
||||||
|
onChange={(event, mask) => {
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||||
|
<OptionIcon type="favorite" isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE} />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
||||||
|
<OptionIcon type="readonly" isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY} />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
||||||
|
<OptionIcon
|
||||||
|
type="api_mqtt_exclude"
|
||||||
|
isSet={(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE}
|
||||||
|
/>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
||||||
|
<OptionIcon
|
||||||
|
type="web_exclude"
|
||||||
|
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
||||||
|
/>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="128">
|
||||||
|
<OptionIcon type="deleted" isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED} />
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityMaskToggle;
|
||||||
@@ -32,9 +32,9 @@ interface OptionIconProps {
|
|||||||
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
|
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
|
||||||
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
|
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
|
||||||
return isSet ? (
|
return isSet ? (
|
||||||
<Icon color="primary" sx={{ fontSize: 14, verticalAlign: 'middle' }} />
|
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||||
) : (
|
) : (
|
||||||
<Icon sx={{ fontSize: 14, verticalAlign: 'middle' }} />
|
<Icon sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
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 PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||||
@@ -17,10 +14,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
Tooltip,
|
|
||||||
Grid,
|
Grid,
|
||||||
TextField,
|
TextField,
|
||||||
Link
|
Link,
|
||||||
|
InputAdornment
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||||
import { useTheme } from '@table-library/react-table-library/theme';
|
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 { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import EntityMaskToggle from './EntityMaskToggle';
|
||||||
import OptionIcon from './OptionIcon';
|
import OptionIcon from './OptionIcon';
|
||||||
|
import SettingsCustomizationDialog from './SettingsCustomizationDialog';
|
||||||
|
|
||||||
import * as EMSESP from './api';
|
import * as EMSESP from './api';
|
||||||
|
|
||||||
import { DeviceEntityMask } from './types';
|
import { DeviceEntityMask } from './types';
|
||||||
import type { DeviceShort, Devices, DeviceEntity } from './types';
|
import type { DeviceShort, Devices, DeviceEntity } from './types';
|
||||||
import type { FC } from 'react';
|
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 RestartMonitor from 'framework/system/RestartMonitor';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import { extractErrorMessage, updateValue } from 'utils';
|
import { extractErrorMessage } from 'utils';
|
||||||
|
|
||||||
export const APIURL = window.location.origin + '/api/';
|
export const APIURL = window.location.origin + '/api/';
|
||||||
|
|
||||||
const SettingsCustomization: FC = () => {
|
const SettingsCustomization: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false };
|
|
||||||
|
|
||||||
const [numChanges, setNumChanges] = useState<number>(0);
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
const blocker = useBlocker(numChanges !== 0);
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
|
|
||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
|
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>();
|
||||||
const [devices, setDevices] = useState<Devices>();
|
const [devices, setDevices] = useState<Devices>();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
|
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
|
||||||
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
||||||
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [deviceEntity, setDeviceEntity] = useState<DeviceEntity>();
|
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||||
// eslint-disable-next-line
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
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 entities_theme = useTheme({
|
const entities_theme = useTheme({
|
||||||
Table: `
|
Table: `
|
||||||
@@ -119,12 +83,10 @@ const SettingsCustomization: FC = () => {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #90CAF9;
|
color: #90CAF9;
|
||||||
|
|
||||||
.th {
|
.th {
|
||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-of-type(1) .th {
|
&:nth-of-type(1) .th {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -133,21 +95,17 @@ const SettingsCustomization: FC = () => {
|
|||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.td {
|
.td {
|
||||||
border-top: 1px solid #565656;
|
border-top: 1px solid #565656;
|
||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.tr.tr-body.row-select.row-select-single-selected {
|
&.tr.tr-body.row-select.row-select-single-selected {
|
||||||
background-color: #3d4752;
|
background-color: #3d4752;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .td {
|
&:hover .td {
|
||||||
border-top: 1px solid #177ac9;
|
border-top: 1px solid #177ac9;
|
||||||
border-bottom: 1px solid #177ac9;
|
border-bottom: 1px solid #177ac9;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-of-type(odd) .td {
|
&:nth-of-type(odd) .td {
|
||||||
background-color: #303030;
|
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 () => {
|
const fetchDevices = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setDevices((await EMSESP.readDevices()).data);
|
setDevices((await EMSESP.readDevices()).data);
|
||||||
@@ -176,6 +156,11 @@ const SettingsCustomization: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [LL]);
|
}, [LL]);
|
||||||
|
|
||||||
|
// on mount
|
||||||
|
useEffect(() => {
|
||||||
|
void fetchDevices();
|
||||||
|
}, [fetchDevices]);
|
||||||
|
|
||||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
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 })));
|
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) {
|
function formatValue(value: any) {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
return new Intl.NumberFormat().format(value);
|
return new Intl.NumberFormat().format(value);
|
||||||
@@ -246,7 +227,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
|
|
||||||
const maskDisabled = (set: boolean) => {
|
const maskDisabled = (set: boolean) => {
|
||||||
setDeviceEntities(
|
setDeviceEntities(
|
||||||
deviceEntities.map(function (de) {
|
deviceEntities?.map(function (de) {
|
||||||
if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) {
|
if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) {
|
||||||
return {
|
return {
|
||||||
...de,
|
...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 () => {
|
const saveCustomization = async () => {
|
||||||
if (devices && deviceEntities && selectedDevice !== -1) {
|
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
|
// check size in bytes to match buffer in CPP, which is 2048
|
||||||
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
|
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
|
||||||
@@ -329,8 +346,8 @@ const SettingsCustomization: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box mb={2} color="warning.main">
|
<Box mb={2} color="warning.main">
|
||||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}</Typography>
|
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2" mt={1}>
|
||||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||||
@@ -338,7 +355,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<ValidatedTextField
|
<TextField
|
||||||
name="device"
|
name="device"
|
||||||
label={LL.EMS_DEVICE()}
|
label={LL.EMS_DEVICE()}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -357,39 +374,16 @@ const SettingsCustomization: FC = () => {
|
|||||||
{device.s}
|
{device.s}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</ValidatedTextField>
|
</TextField>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editEntity = (de: DeviceEntity) => {
|
const renderDeviceData = () => {
|
||||||
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
if (!deviceEntities) {
|
||||||
return;
|
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 === '') {
|
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -401,33 +395,23 @@ const SettingsCustomization: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
|
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
|
||||||
<Grid item>
|
|
||||||
<Typography variant="subtitle2" color="primary">
|
|
||||||
#:
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
{shown_data.length}/{deviceEntities.length}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<SearchIcon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />:
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={2}>
|
<Grid item xs={2}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
placeholder={LL.SEARCH()}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setSearch(event.target.value);
|
setSearch(event.target.value);
|
||||||
}}
|
}}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Tooltip arrow placement="top" title="apply filter">
|
|
||||||
<Grid item>
|
|
||||||
<FilterListIcon color="primary" sx={{ fontSize: 14, verticalAlign: 'middle' }} />:
|
|
||||||
</Grid>
|
|
||||||
</Tooltip>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
size="small"
|
size="small"
|
||||||
@@ -454,7 +438,6 @@ const SettingsCustomization: FC = () => {
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -481,6 +464,11 @@ const SettingsCustomization: FC = () => {
|
|||||||
<OptionIcon type="web_exclude" isSet={true} />
|
<OptionIcon type="web_exclude" isSet={true} />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="subtitle2" color="primary">
|
||||||
|
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
||||||
{(tableList: any) => (
|
{(tableList: any) => (
|
||||||
@@ -496,63 +484,14 @@ const SettingsCustomization: FC = () => {
|
|||||||
</Header>
|
</Header>
|
||||||
<Body>
|
<Body>
|
||||||
{tableList.map((de: DeviceEntity) => (
|
{tableList.map((de: DeviceEntity) => (
|
||||||
<Row key={de.id} item={de} onClick={() => editEntity(de)}>
|
<Row key={de.id} item={de} onClick={() => editDeviceEntity(de)}>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
{!deviceEntity && (
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
||||||
<ToggleButtonGroup
|
|
||||||
size="small"
|
|
||||||
color="secondary"
|
|
||||||
value={getMaskString(de.m)}
|
|
||||||
onChange={(event, mask) => {
|
|
||||||
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
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
|
||||||
<OptionIcon
|
|
||||||
type="favorite"
|
|
||||||
isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE}
|
|
||||||
/>
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
|
||||||
<OptionIcon
|
|
||||||
type="readonly"
|
|
||||||
isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY}
|
|
||||||
/>
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
|
||||||
<OptionIcon
|
|
||||||
type="api_mqtt_exclude"
|
|
||||||
isSet={
|
|
||||||
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
|
||||||
<OptionIcon
|
|
||||||
type="web_exclude"
|
|
||||||
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
|
||||||
/>
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton value="128">
|
|
||||||
<OptionIcon
|
|
||||||
type="deleted"
|
|
||||||
isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED}
|
|
||||||
/>
|
|
||||||
</ToggleButton>
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
)}
|
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{!deviceEntity && formatName(de)}</Cell>
|
<Cell>{formatName(de)}</Cell>
|
||||||
<Cell>{!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
||||||
<Cell>{!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
||||||
<Cell>{!deviceEntity && formatValue(de.v)}</Cell>
|
<Cell>{formatValue(de.v)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Body>
|
</Body>
|
||||||
@@ -632,83 +571,18 @@ const SettingsCustomization: FC = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderEditDialog = () => {
|
|
||||||
if (deviceEntity) {
|
|
||||||
return (
|
|
||||||
<Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}>
|
|
||||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + deviceEntity.id + '"'}</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Box color="warning.main" mb={2}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {deviceEntity.n}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Grid container spacing={1}>
|
|
||||||
<Grid item>
|
|
||||||
<TextField
|
|
||||||
name="cn"
|
|
||||||
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
|
||||||
value={deviceEntity.cn}
|
|
||||||
autoFocus
|
|
||||||
sx={{ width: '30ch' }}
|
|
||||||
onChange={updateValue(setDeviceEntity)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
{typeof deviceEntity.v === 'number' &&
|
|
||||||
deviceEntity.w &&
|
|
||||||
!(deviceEntity.m & DeviceEntityMask.DV_READONLY) && (
|
|
||||||
<>
|
|
||||||
<Grid item>
|
|
||||||
<TextField
|
|
||||||
name="mi"
|
|
||||||
label={LL.MIN()}
|
|
||||||
value={deviceEntity.mi}
|
|
||||||
sx={{ width: '8ch' }}
|
|
||||||
onChange={updateValue(setDeviceEntity)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<TextField
|
|
||||||
name="ma"
|
|
||||||
label={LL.MAX()}
|
|
||||||
value={deviceEntity.ma}
|
|
||||||
sx={{ width: '8ch' }}
|
|
||||||
onChange={updateValue(setDeviceEntity)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
startIcon={<CancelIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => setDeviceEntity(undefined)}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<DoneIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => updateEntity()}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{LL.UPDATE()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <RestartMonitor /> : renderContent()}
|
{restarting ? <RestartMonitor /> : renderContent()}
|
||||||
{renderEditDialog()}
|
{selectedDeviceEntity && (
|
||||||
|
<SettingsCustomizationDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
onSave={onDialogSave}
|
||||||
|
selectedDeviceEntity={selectedDeviceEntity}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
141
interface/src/project/SettingsCustomizationDialog.tsx
Normal file
141
interface/src/project/SettingsCustomizationDialog.tsx
Normal file
@@ -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<DeviceEntity>(selectedDeviceEntity);
|
||||||
|
const [error, setError] = useState<boolean>(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 (
|
||||||
|
<Dialog open={open} onClose={close}>
|
||||||
|
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY()}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box color="warning.main">
|
||||||
|
<Typography variant="body2">
|
||||||
|
{LL.ENTITY()}: {editItem.id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box color="warning.main" mt={1} mb={2}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {editItem.n}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box mb={3}>
|
||||||
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||||
|
</Box>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
name="cn"
|
||||||
|
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
||||||
|
value={editItem.cn}
|
||||||
|
autoFocus
|
||||||
|
sx={{ width: '30ch' }}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{isWriteableNumber && (
|
||||||
|
<>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
name="mi"
|
||||||
|
label={LL.MIN()}
|
||||||
|
value={editItem.mi}
|
||||||
|
sx={{ width: '8ch' }}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
name="ma"
|
||||||
|
label={LL.MAX()}
|
||||||
|
value={editItem.ma}
|
||||||
|
sx={{ width: '8ch' }}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
{error && (
|
||||||
|
<Typography variant="body2" color="error" mt={2}>
|
||||||
|
Error: Check min and max values
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
|
||||||
|
{LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsCustomizationDialog;
|
||||||
Reference in New Issue
Block a user