mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
808 lines
23 KiB
TypeScript
808 lines
23 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useBlocker, useLocation } from 'react-router';
|
|
import { toast } from 'react-toastify';
|
|
|
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
import EditIcon from '@mui/icons-material/Edit';
|
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
|
import SaveIcon from '@mui/icons-material/Save';
|
|
import SearchIcon from '@mui/icons-material/Search';
|
|
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
|
import WarningIcon from '@mui/icons-material/Warning';
|
|
import {
|
|
Box,
|
|
Button,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Grid,
|
|
InputAdornment,
|
|
Link,
|
|
MenuItem,
|
|
TextField,
|
|
ToggleButton,
|
|
ToggleButtonGroup,
|
|
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 { dialogStyle } from 'CustomTheme';
|
|
import { useRequest } from 'alova/client';
|
|
import SystemMonitor from 'app/status/SystemMonitor';
|
|
import {
|
|
BlockNavigation,
|
|
ButtonRow,
|
|
MessageBox,
|
|
SectionContent,
|
|
useLayoutTitle
|
|
} from 'components';
|
|
import { useI18nContext } from 'i18n/i18n-react';
|
|
|
|
import {
|
|
API,
|
|
readCoreData,
|
|
readDeviceEntities,
|
|
resetCustomizations,
|
|
writeCustomizationEntities,
|
|
writeDeviceName
|
|
} from '../../api/app';
|
|
import SettingsCustomizationsDialog from './CustomizationsDialog';
|
|
import EntityMaskToggle from './EntityMaskToggle';
|
|
import OptionIcon from './OptionIcon';
|
|
import { DeviceEntityMask } from './types';
|
|
import type { APIcall, Device, DeviceEntity } from './types';
|
|
|
|
export const APIURL = `${window.location.origin}/api/`;
|
|
|
|
const MAX_BUFFER_SIZE = 2000;
|
|
|
|
// Helper function to create masked entity ID - extracted to avoid duplication
|
|
const createMaskedEntityId = (de: DeviceEntity): string => {
|
|
const maskHex = de.m.toString(16).padStart(2, '0');
|
|
const hasCustomizations = !!(de.cn || de.mi || de.ma);
|
|
const customizations = [
|
|
de.cn || '',
|
|
de.mi ? `>${de.mi}` : '',
|
|
de.ma ? `<${de.ma}` : ''
|
|
]
|
|
.filter(Boolean)
|
|
.join('');
|
|
|
|
return `${maskHex}${de.id}${hasCustomizations ? `|${customizations}` : ''}`;
|
|
};
|
|
|
|
const Customizations = () => {
|
|
const { LL } = useI18nContext();
|
|
const [numChanges, setNumChanges] = useState<number>(0);
|
|
const blocker = useBlocker(numChanges !== 0);
|
|
|
|
const [restarting, setRestarting] = useState<boolean>(false);
|
|
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
|
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
|
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
|
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
|
const [search, setSearch] = useState('');
|
|
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
|
const [rename, setRename] = useState<boolean>(false);
|
|
|
|
useLayoutTitle(LL.CUSTOMIZATIONS());
|
|
|
|
// fetch devices first from coreData
|
|
const { data: devices, send: fetchCoreData } = useRequest(readCoreData);
|
|
|
|
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
|
immediate: false
|
|
});
|
|
|
|
const [selectedDevice, setSelectedDevice] = useState<number>(
|
|
Number(useLocation().state) || -1
|
|
);
|
|
const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] =
|
|
useState<string>(''); // needed for API URL
|
|
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
|
|
|
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
|
immediate: false
|
|
});
|
|
|
|
const { send: sendDeviceName } = useRequest(
|
|
(data: { id: number; name: string }) => writeDeviceName(data),
|
|
{
|
|
immediate: false
|
|
}
|
|
);
|
|
|
|
const { send: sendCustomizationEntities } = useRequest(
|
|
(data: { id: number; entity_ids: string[] }) => writeCustomizationEntities(data),
|
|
{
|
|
immediate: false
|
|
}
|
|
);
|
|
|
|
const { send: sendDeviceEntities } = useRequest(
|
|
(data: number) => readDeviceEntities(data),
|
|
{
|
|
initialData: [],
|
|
immediate: false
|
|
}
|
|
).onSuccess((event) => {
|
|
setOriginalSettings(event.data);
|
|
});
|
|
|
|
const setOriginalSettings = (data: DeviceEntity[]) => {
|
|
setDeviceEntities(
|
|
data.map((de) => {
|
|
const result: DeviceEntity = {
|
|
...de,
|
|
o_m: de.m
|
|
};
|
|
if (de.cn !== undefined) {
|
|
result.o_cn = de.cn;
|
|
}
|
|
if (de.mi !== undefined) {
|
|
result.o_mi = de.mi;
|
|
}
|
|
if (de.ma !== undefined) {
|
|
result.o_ma = de.ma;
|
|
}
|
|
return result;
|
|
})
|
|
);
|
|
};
|
|
|
|
const doRestart = async () => {
|
|
setRestarting(true);
|
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
|
(error: Error) => {
|
|
toast.error(error.message);
|
|
}
|
|
);
|
|
};
|
|
|
|
const entities_theme = useMemo(
|
|
() =>
|
|
useTheme({
|
|
Table: `
|
|
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
|
`,
|
|
BaseRow: `
|
|
font-size: 14px;
|
|
.td {
|
|
height: 32px;
|
|
}
|
|
`,
|
|
BaseCell: `
|
|
&:nth-of-type(3) {
|
|
text-align: right;
|
|
}
|
|
&:nth-of-type(4) {
|
|
text-align: right;
|
|
}
|
|
&:last-of-type {
|
|
text-align: right;
|
|
}
|
|
`,
|
|
HeaderRow: `
|
|
text-transform: uppercase;
|
|
background-color: black;
|
|
color: #90CAF9;
|
|
.th {
|
|
border-bottom: 1px solid #565656;
|
|
height: 36px;
|
|
}
|
|
&:nth-of-type(1) .th {
|
|
text-align: center;
|
|
}
|
|
`,
|
|
Row: `
|
|
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;
|
|
background-color: #177ac9;
|
|
}
|
|
`,
|
|
Cell: `
|
|
&:nth-of-type(2) {
|
|
padding: 8px;
|
|
}
|
|
&:nth-of-type(3) {
|
|
padding-right: 4px;
|
|
}
|
|
&:nth-of-type(4) {
|
|
padding-right: 4px;
|
|
}
|
|
&:last-of-type {
|
|
padding-right: 8px;
|
|
}
|
|
`
|
|
}),
|
|
[]
|
|
);
|
|
|
|
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.length) {
|
|
const changedEntities = deviceEntities.filter((de) => hasEntityChanged(de));
|
|
setNumChanges(changedEntities.length);
|
|
}
|
|
}, [deviceEntities]);
|
|
|
|
useEffect(() => {
|
|
if (devices && selectedDevice !== -1) {
|
|
void sendDeviceEntities(selectedDevice);
|
|
const index = devices.devices.findIndex((d) => d.id === selectedDevice);
|
|
if (index === -1) {
|
|
setSelectedDevice(-1);
|
|
setSelectedDeviceTypeNameURL('');
|
|
} else {
|
|
const device = devices.devices[index];
|
|
if (device) {
|
|
setSelectedDeviceTypeNameURL(device.url || '');
|
|
setSelectedDeviceName(device.n);
|
|
}
|
|
setNumChanges(0);
|
|
setRestartNeeded(false);
|
|
}
|
|
}
|
|
}, [devices, selectedDevice]);
|
|
|
|
function formatValue(value: unknown) {
|
|
if (typeof value === 'number') {
|
|
return new Intl.NumberFormat().format(value);
|
|
} else if (value === undefined) {
|
|
return '';
|
|
} else if (typeof value === 'boolean') {
|
|
return value ? 'true' : 'false';
|
|
}
|
|
return value as string;
|
|
}
|
|
|
|
const isCommand = useCallback((de: DeviceEntity) => {
|
|
return de.n && de.n[0] === '!';
|
|
}, []);
|
|
|
|
const formatName = useCallback(
|
|
(de: DeviceEntity, withShortname: boolean) => {
|
|
let name: string;
|
|
if (isCommand(de)) {
|
|
name = de.t
|
|
? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}`
|
|
: `${LL.COMMAND(1)}: ${de.n?.slice(1)}`;
|
|
} else if (de.cn && de.cn !== '') {
|
|
name = de.t ? `${de.t} ${de.cn}` : de.cn;
|
|
} else {
|
|
name = de.t ? `${de.t} ${de.n}` : de.n || '';
|
|
}
|
|
return withShortname ? `${name} ${de.id}` : name;
|
|
},
|
|
[LL]
|
|
);
|
|
|
|
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;
|
|
};
|
|
|
|
const filter_entity = useCallback(
|
|
(de: DeviceEntity) =>
|
|
(de.m & selectedFilters || !selectedFilters) &&
|
|
formatName(de, true).toLowerCase().includes(search.toLowerCase()),
|
|
[selectedFilters, search, formatName]
|
|
);
|
|
|
|
const maskDisabled = useCallback(
|
|
(set: boolean) => {
|
|
setDeviceEntities((prev) =>
|
|
prev.map((de) => {
|
|
if (filter_entity(de)) {
|
|
const excludeMask =
|
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
|
|
return {
|
|
...de,
|
|
m: set ? de.m | excludeMask : de.m & ~excludeMask
|
|
};
|
|
}
|
|
return de;
|
|
})
|
|
);
|
|
},
|
|
[filter_entity]
|
|
);
|
|
|
|
const resetCustomization = useCallback(async () => {
|
|
try {
|
|
await sendResetCustomizations();
|
|
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
|
} catch (error) {
|
|
toast.error((error as Error).message);
|
|
} finally {
|
|
setConfirmReset(false);
|
|
setRestarting(true);
|
|
}
|
|
}, [sendResetCustomizations, LL]);
|
|
|
|
const onDialogClose = () => {
|
|
setDialogOpen(false);
|
|
};
|
|
|
|
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
|
|
setDeviceEntities(
|
|
(prev) =>
|
|
prev?.map((de) =>
|
|
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
|
) ?? []
|
|
);
|
|
}, []);
|
|
|
|
const onDialogSave = useCallback(
|
|
(updatedItem: DeviceEntity) => {
|
|
setDialogOpen(false);
|
|
updateDeviceEntity(updatedItem);
|
|
},
|
|
[updateDeviceEntity]
|
|
);
|
|
|
|
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 = useCallback(async () => {
|
|
if (!devices || !deviceEntities || selectedDevice === -1) {
|
|
return;
|
|
}
|
|
|
|
const masked_entities = deviceEntities
|
|
.filter((de: DeviceEntity) => hasEntityChanged(de))
|
|
.map((new_de) => createMaskedEntityId(new_de));
|
|
|
|
// check size in bytes to match buffer in CPP, which is 2048
|
|
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
|
|
if (bytes > MAX_BUFFER_SIZE) {
|
|
toast.warning(LL.CUSTOMIZATIONS_FULL());
|
|
return;
|
|
}
|
|
|
|
await sendCustomizationEntities({
|
|
id: selectedDevice,
|
|
entity_ids: masked_entities
|
|
})
|
|
.then(() => {
|
|
toast.success(LL.CUSTOMIZATIONS_SAVED());
|
|
})
|
|
.catch((error: Error) => {
|
|
if (error.message === 'Reboot required') {
|
|
setRestartNeeded(true);
|
|
} else {
|
|
toast.error(error.message);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
setOriginalSettings(deviceEntities);
|
|
});
|
|
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]);
|
|
|
|
const renameDevice = useCallback(async () => {
|
|
await sendDeviceName({ id: selectedDevice, name: selectedDeviceName })
|
|
.then(() => {
|
|
toast.success(LL.UPDATED_OF(LL.NAME(1)));
|
|
})
|
|
.catch(() => {
|
|
toast.error(`${LL.UPDATE_OF(LL.NAME(1))} ${LL.FAILED(1)}`);
|
|
})
|
|
.finally(async () => {
|
|
setRename(false);
|
|
await fetchCoreData();
|
|
});
|
|
}, [selectedDevice, selectedDeviceName, sendDeviceName, LL, fetchCoreData]);
|
|
|
|
const renderDeviceList = () => (
|
|
<>
|
|
<Box mb={1} color="warning.main">
|
|
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
|
</Box>
|
|
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
|
{rename ? (
|
|
<TextField
|
|
name="device"
|
|
label={LL.EMS_DEVICE()}
|
|
fullWidth
|
|
variant="outlined"
|
|
value={selectedDeviceName}
|
|
onChange={(e) => setSelectedDeviceName(e.target.value)}
|
|
margin="normal"
|
|
/>
|
|
) : (
|
|
<TextField
|
|
name="device"
|
|
label={LL.EMS_DEVICE()}
|
|
variant="outlined"
|
|
value={selectedDevice}
|
|
disabled={numChanges !== 0}
|
|
onChange={(e) => setSelectedDevice(parseInt(e.target.value))}
|
|
margin="normal"
|
|
style={{ minWidth: '50%' }}
|
|
select
|
|
>
|
|
<MenuItem disabled key={-1} value={-1}>
|
|
{LL.SELECT_DEVICE()}...
|
|
</MenuItem>
|
|
{devices.devices.map(
|
|
(device: Device) =>
|
|
device.id < 90 && (
|
|
<MenuItem key={device.id} value={device.id}>
|
|
{device.n} ({device.tn})
|
|
</MenuItem>
|
|
)
|
|
)}
|
|
</TextField>
|
|
)}
|
|
{selectedDevice !== -1 &&
|
|
(rename ? (
|
|
<>
|
|
<Button
|
|
startIcon={<CancelIcon />}
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => setRename(false)}
|
|
>
|
|
{LL.CANCEL()}
|
|
</Button>
|
|
<Button
|
|
startIcon={<SaveIcon />}
|
|
variant="outlined"
|
|
onClick={() => renameDevice()}
|
|
>
|
|
{LL.RENAME()}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button
|
|
startIcon={<EditIcon />}
|
|
variant="outlined"
|
|
onClick={() => setRename(true)}
|
|
>
|
|
{LL.RENAME()}
|
|
</Button>
|
|
<Button
|
|
startIcon={<SettingsBackupRestoreIcon />}
|
|
variant="outlined"
|
|
color="error"
|
|
onClick={() => setConfirmReset(true)}
|
|
>
|
|
{LL.REMOVE_ALL()}
|
|
</Button>
|
|
</>
|
|
))}
|
|
</Box>
|
|
</>
|
|
);
|
|
|
|
const filteredEntities = useMemo(
|
|
() => deviceEntities.filter((de) => filter_entity(de)),
|
|
[deviceEntities, filter_entity]
|
|
);
|
|
|
|
const renderDeviceData = () => {
|
|
return (
|
|
<>
|
|
<Box color="warning.main">
|
|
<Typography variant="body2" mt={1} mb={1}>
|
|
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
|
|
|
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
|
|
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
|
{LL.CUSTOMIZATIONS_HELP_4()}
|
|
<OptionIcon type="web_exclude" isSet={true} />=
|
|
{LL.CUSTOMIZATIONS_HELP_5()}
|
|
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
|
</Typography>
|
|
</Box>
|
|
<Grid
|
|
container
|
|
mb={1}
|
|
mt={0}
|
|
spacing={2}
|
|
direction="row"
|
|
justifyContent="flex-start"
|
|
alignItems="center"
|
|
>
|
|
<Grid>
|
|
<TextField
|
|
size="small"
|
|
variant="outlined"
|
|
placeholder={LL.SEARCH()}
|
|
aria-label={LL.SEARCH()}
|
|
onChange={(event) => {
|
|
setSearch(event.target.value);
|
|
}}
|
|
slotProps={{
|
|
input: {
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
|
</InputAdornment>
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
</Grid>
|
|
<Grid>
|
|
<ToggleButtonGroup
|
|
size="small"
|
|
color="secondary"
|
|
value={getMaskString(selectedFilters)}
|
|
onChange={(_, mask: string[]) => {
|
|
setSelectedFilters(getMaskNumber(mask));
|
|
}}
|
|
>
|
|
<ToggleButton value="8">
|
|
<OptionIcon type="favorite" isSet={true} />
|
|
</ToggleButton>
|
|
<ToggleButton value="4">
|
|
<OptionIcon type="readonly" isSet={true} />
|
|
</ToggleButton>
|
|
<ToggleButton value="2">
|
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
|
</ToggleButton>
|
|
<ToggleButton value="1">
|
|
<OptionIcon type="web_exclude" isSet={true} />
|
|
</ToggleButton>
|
|
<ToggleButton value="128">
|
|
<OptionIcon type="deleted" isSet={true} />
|
|
</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Grid>
|
|
<Grid>
|
|
<Button
|
|
size="small"
|
|
sx={{ fontSize: 10 }}
|
|
variant="outlined"
|
|
color="inherit"
|
|
onClick={() => maskDisabled(false)}
|
|
>
|
|
{LL.SET_ALL()}
|
|
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
|
<OptionIcon type="web_exclude" isSet={false} />
|
|
</Button>
|
|
</Grid>
|
|
<Grid>
|
|
<Button
|
|
size="small"
|
|
sx={{ fontSize: 10 }}
|
|
variant="outlined"
|
|
color="inherit"
|
|
onClick={() => maskDisabled(true)}
|
|
>
|
|
{LL.SET_ALL()}
|
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
|
<OptionIcon type="web_exclude" isSet={true} />
|
|
</Button>
|
|
</Grid>
|
|
<Grid>
|
|
<Typography variant="subtitle2" color="grey">
|
|
{LL.SHOWING()} {filteredEntities.length}/{deviceEntities.length}
|
|
{LL.ENTITIES(deviceEntities.length)}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
<Table
|
|
data={{ nodes: filteredEntities }}
|
|
theme={entities_theme}
|
|
layout={{ custom: true }}
|
|
>
|
|
{(tableList: DeviceEntity[]) => (
|
|
<>
|
|
<Header>
|
|
<HeaderRow>
|
|
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
|
|
<HeaderCell resize>{LL.NAME(1)}</HeaderCell>
|
|
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
|
|
<HeaderCell stiff>{LL.MAX()}</HeaderCell>
|
|
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
|
|
</HeaderRow>
|
|
</Header>
|
|
<Body>
|
|
{tableList.map((de: DeviceEntity) => (
|
|
<Row key={de.id} item={de} onClick={() => editDeviceEntity(de)}>
|
|
<Cell stiff>
|
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
|
</Cell>
|
|
<Cell>
|
|
<span
|
|
style={{
|
|
color:
|
|
de.v === undefined && !isCommand(de) ? 'grey' : 'inherit'
|
|
}}
|
|
>
|
|
{formatName(de, false)} (
|
|
<Link
|
|
style={{
|
|
color:
|
|
de.v === undefined && !isCommand(de)
|
|
? 'grey'
|
|
: 'primary'
|
|
}}
|
|
target="_blank"
|
|
href={APIURL + selectedDeviceTypeNameURL + '/' + de.id}
|
|
>
|
|
{de.id}
|
|
</Link>
|
|
)
|
|
</span>
|
|
</Cell>
|
|
<Cell>
|
|
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
|
|
</Cell>
|
|
<Cell>
|
|
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
|
|
</Cell>
|
|
<Cell>{formatValue(de.v)}</Cell>
|
|
</Row>
|
|
))}
|
|
</Body>
|
|
</>
|
|
)}
|
|
</Table>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const renderResetDialog = () => (
|
|
<Dialog
|
|
sx={dialogStyle}
|
|
open={confirmReset}
|
|
onClose={() => setConfirmReset(false)}
|
|
>
|
|
<DialogTitle>{LL.REMOVE_ALL()}</DialogTitle>
|
|
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
startIcon={<CancelIcon />}
|
|
variant="outlined"
|
|
onClick={() => setConfirmReset(false)}
|
|
color="secondary"
|
|
>
|
|
{LL.CANCEL()}
|
|
</Button>
|
|
<Button
|
|
startIcon={<SettingsBackupRestoreIcon />}
|
|
variant="outlined"
|
|
onClick={resetCustomization}
|
|
color="error"
|
|
>
|
|
{LL.REMOVE_ALL()}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
|
|
const renderContent = () => (
|
|
<>
|
|
{devices && renderDeviceList()}
|
|
{selectedDevice !== -1 && !rename && renderDeviceData()}
|
|
{restartNeeded ? (
|
|
<MessageBox level="warning" message={LL.RESTART_TEXT(0)}>
|
|
<Button
|
|
sx={{ ml: 2 }}
|
|
startIcon={<PowerSettingsNewIcon />}
|
|
variant="contained"
|
|
color="error"
|
|
onClick={doRestart}
|
|
>
|
|
{LL.RESTART()}
|
|
</Button>
|
|
</MessageBox>
|
|
) : (
|
|
<Box display="flex" flexWrap="wrap">
|
|
<Box flexGrow={1}>
|
|
{numChanges !== 0 && (
|
|
<ButtonRow>
|
|
<Button
|
|
startIcon={<CancelIcon />}
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => {
|
|
if (devices) {
|
|
void sendDeviceEntities(selectedDevice);
|
|
}
|
|
}}
|
|
>
|
|
{LL.CANCEL()}
|
|
</Button>
|
|
<Button
|
|
startIcon={<WarningIcon color="warning" />}
|
|
variant="contained"
|
|
color="info"
|
|
onClick={saveCustomization}
|
|
>
|
|
{LL.APPLY_CHANGES(numChanges)}
|
|
</Button>
|
|
</ButtonRow>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
{renderResetDialog()}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<SectionContent>
|
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
|
{restarting ? <SystemMonitor /> : renderContent()}
|
|
{selectedDeviceEntity && (
|
|
<SettingsCustomizationsDialog
|
|
open={dialogOpen}
|
|
onClose={onDialogClose}
|
|
onSave={onDialogSave}
|
|
selectedItem={selectedDeviceEntity}
|
|
/>
|
|
)}
|
|
</SectionContent>
|
|
);
|
|
};
|
|
|
|
export default Customizations;
|