mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 08:49:52 +03:00
Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev2
This commit is contained in:
@@ -20,7 +20,8 @@ import {
|
||||
ValidatedTextField,
|
||||
ButtonRow,
|
||||
MessageBox,
|
||||
BlockNavigation
|
||||
BlockNavigation,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
|
||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
||||
@@ -36,7 +37,7 @@ export function boardProfileSelectItems() {
|
||||
));
|
||||
}
|
||||
|
||||
const SettingsApplication: FC = () => {
|
||||
const ApplicationSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saveData,
|
||||
@@ -97,6 +98,8 @@ const SettingsApplication: FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.APPLICATION_SETTINGS());
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
@@ -136,7 +139,7 @@ const SettingsApplication: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
<Typography sx={{ pb: 1 }} variant="h6" color="primary">
|
||||
{LL.INTERFACE_BOARD_PROFILE()}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
@@ -680,11 +683,11 @@ const SettingsApplication: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsApplication;
|
||||
export default ApplicationSettings;
|
||||
@@ -13,17 +13,17 @@ import { useBlocker } from 'react-router-dom';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import SettingsCustomEntitiesDialog from './SettingsCustomEntitiesDialog';
|
||||
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
|
||||
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 { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const SettingsCustomEntities: FC = () => {
|
||||
const CustomEntities: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -31,6 +31,8 @@ const SettingsCustomEntities: FC = () => {
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
|
||||
|
||||
const {
|
||||
data: entities,
|
||||
send: fetchEntities,
|
||||
@@ -246,7 +248,7 @@ const SettingsCustomEntities: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
|
||||
@@ -265,7 +267,7 @@ const SettingsCustomEntities: FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges > 0 && (
|
||||
<ButtonRow>
|
||||
@@ -298,4 +300,4 @@ const SettingsCustomEntities: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCustomEntities;
|
||||
export default CustomEntities;
|
||||
@@ -30,7 +30,7 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
type SettingsCustomEntitiesDialogProps = {
|
||||
type CustomEntitiesDialogProps = {
|
||||
open: boolean;
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
@@ -39,14 +39,14 @@ type SettingsCustomEntitiesDialogProps = {
|
||||
validator: Schema;
|
||||
};
|
||||
|
||||
const SettingsCustomEntitiesDialog = ({
|
||||
const CustomEntitiesDialog = ({
|
||||
open,
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator
|
||||
}: SettingsCustomEntitiesDialogProps) => {
|
||||
}: CustomEntitiesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -281,4 +281,4 @@ const SettingsCustomEntitiesDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCustomEntitiesDialog;
|
||||
export default CustomEntitiesDialog;
|
||||
@@ -26,9 +26,9 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useBlocker, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import SettingsCustomizationDialog from './CustomizationDialog';
|
||||
import EntityMaskToggle from './EntityMaskToggle';
|
||||
import OptionIcon from './OptionIcon';
|
||||
import SettingsCustomizationDialog from './SettingsCustomizationDialog';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
@@ -37,14 +37,14 @@ import type { DeviceShort, DeviceEntity } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
|
||||
import { ButtonRow, SectionContent, MessageBox, BlockNavigation, useLayoutTitle } from 'components';
|
||||
|
||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
export const APIURL = window.location.origin + '/api/';
|
||||
|
||||
const SettingsCustomization: FC = () => {
|
||||
const Customization: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -58,6 +58,8 @@ const SettingsCustomization: FC = () => {
|
||||
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.CUSTOMIZATIONS());
|
||||
|
||||
// fetch devices first
|
||||
const { data: devices } = useRequest(EMSESP.readDevices);
|
||||
|
||||
@@ -508,9 +510,6 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DEVICE_ENTITIES()}
|
||||
</Typography>
|
||||
{devices && renderDeviceList()}
|
||||
{deviceEntities && renderDeviceData()}
|
||||
{restartNeeded && (
|
||||
@@ -544,7 +543,7 @@ const SettingsCustomization: FC = () => {
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
<ButtonRow>
|
||||
<ButtonRow mt={1}>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
@@ -561,7 +560,7 @@ const SettingsCustomization: FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : renderContent()}
|
||||
{selectedDeviceEntity && (
|
||||
@@ -576,4 +575,4 @@ const SettingsCustomization: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCustomization;
|
||||
export default Customization;
|
||||
@@ -31,7 +31,7 @@ type SettingsCustomizationDialogProps = {
|
||||
selectedItem: DeviceEntity;
|
||||
};
|
||||
|
||||
const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
|
||||
const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
@@ -152,4 +152,4 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCustomizationDialog;
|
||||
export default CustomizationDialog;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import DashboardDevices from './DashboardDevices';
|
||||
import DashboardSensors from './DashboardSensors';
|
||||
import DashboardStatus from './DashboardStatus';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Dashboard: FC = () => {
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.DASHBOARD());
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/dashboard/devices" label={LL.DEVICES()} />
|
||||
<Tab value="/dashboard/sensors" label={LL.SENSORS()} />
|
||||
<Tab value="/dashboard/status" label="Status" />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="devices" element={<DashboardDevices />} />
|
||||
<Route path="sensors" element={<DashboardSensors />} />
|
||||
<Route path="status" element={<DashboardStatus />} />
|
||||
<Route path="*" element={<Navigate replace to="/dashboard/devices" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,282 +0,0 @@
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import { busConnectionStatus } from './types';
|
||||
import type { Stat, Status } from './types';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import type { Translation } from 'i18n/i18n-types';
|
||||
import type { FC } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
|
||||
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const showQuality = (stat: Stat) => {
|
||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||
return;
|
||||
}
|
||||
if (stat.q === 100) {
|
||||
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
|
||||
}
|
||||
if (stat.q >= 95) {
|
||||
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
|
||||
} else {
|
||||
return <div style={{ color: 'red' }}>{stat.q}%</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const DashboardStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(EMSESP.readStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
const [confirmScan, setConfirmScan] = useState<boolean>(false);
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const stats_theme = tableTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
|
||||
.th {
|
||||
height: 36px;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
&:nth-of-type(even) .td {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:not(:first-of-type) {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => loadData(), 30000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
|
||||
const showName = (id: any) => {
|
||||
const name: keyof Translation['STATUS_NAMES'] = id;
|
||||
return LL.STATUS_NAMES[name]();
|
||||
};
|
||||
|
||||
const formatDurationSec = (duration_sec: number) => {
|
||||
const days = Math.trunc((duration_sec * 1000) / 86400000);
|
||||
const hours = Math.trunc((duration_sec * 1000) / 3600000) % 24;
|
||||
const minutes = Math.trunc((duration_sec * 1000) / 60000) % 60;
|
||||
const seconds = Math.trunc((duration_sec * 1000) / 1000) % 60;
|
||||
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += LL.NUM_DAYS({ num: days }) + ' ';
|
||||
}
|
||||
if (hours) {
|
||||
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
|
||||
}
|
||||
formatted += LL.NUM_SECONDS({ num: seconds });
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const busStatus = () => {
|
||||
if (data) {
|
||||
switch (data.status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (' + formatDurationSec(data.uptime) + ')';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return LL.TX_ISSUES();
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return LL.DISCONNECTED();
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
const scan = async () => {
|
||||
await scanDevices()
|
||||
.then(() => {
|
||||
toast.info(LL.SCANNING() + '...');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
setConfirmScan(false);
|
||||
};
|
||||
|
||||
const renderScanDialog = () => (
|
||||
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}>
|
||||
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
|
||||
{LL.SCAN()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: busStatusHighlight(data, theme) }}>
|
||||
<DirectionsBusIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus()} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.ACTIVE_DEVICES()}
|
||||
secondary={
|
||||
LL.NUM_DEVICES({ num: data.num_devices }) +
|
||||
', ' +
|
||||
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
|
||||
', ' +
|
||||
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Box m={3} />
|
||||
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize />
|
||||
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((stat: Stat) => (
|
||||
<Row key={stat.id} item={stat}>
|
||||
<Cell>{showName(stat.id)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
|
||||
<Cell stiff>{showQuality(stat)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
</List>
|
||||
{renderScanDialog()}
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<PermScanWifiIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
disabled={!me.admin}
|
||||
onClick={() => setConfirmScan(true)}
|
||||
>
|
||||
{LL.SCAN_DEVICES()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.EMS_BUS_STATUS_TITLE()} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardStatus;
|
||||
@@ -1,12 +1,13 @@
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert } from 'react-icons/ai';
|
||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||
|
||||
import { FaSolarPanel } from 'react-icons/fa';
|
||||
import { GiHeatHaze, GiTap } from 'react-icons/gi';
|
||||
import { MdThermostatAuto, MdOutlineSensors, MdOutlineExtension, MdOutlineDevices, MdOutlinePool } from 'react-icons/md';
|
||||
import { MdThermostatAuto, MdOutlineSensors, MdOutlineDevices, MdOutlinePool } from 'react-icons/md';
|
||||
import { TiFlowSwitch } from 'react-icons/ti';
|
||||
import { VscVmConnect } from 'react-icons/vsc';
|
||||
import { DeviceType } from './types';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
interface DeviceIconProps {
|
||||
@@ -45,7 +46,7 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
|
||||
case DeviceType.POOL:
|
||||
return <MdOutlinePool />;
|
||||
case DeviceType.CUSTOM:
|
||||
return <MdOutlineExtension />;
|
||||
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,13 @@ import { useSort, SortToggleType } from '@table-library/react-table-library/sort
|
||||
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 { useRequest } from 'alova';
|
||||
import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react';
|
||||
import { useState, useEffect, useCallback, useLayoutEffect, useContext } from 'react';
|
||||
|
||||
import { IconContext } from 'react-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import DashboardDevicesDialog from './DashboardDevicesDialog';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DashboardDevicesDialog from './DevicesDialog';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import { formatValue } from './deviceValue';
|
||||
@@ -49,14 +49,15 @@ import { deviceValueItemValidation } from './validators';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { ButtonRow, SectionContent, MessageBox } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { ButtonRow, SectionContent, MessageBox, useLayoutTitle } from 'components';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const DashboardDevices: FC = () => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const Devices: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [size, setSize] = useState([0, 0]);
|
||||
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
|
||||
const [onlyFav, setOnlyFav] = useState(false);
|
||||
@@ -66,6 +67,8 @@ const DashboardDevices: FC = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useLayoutTitle(LL.DEVICES());
|
||||
|
||||
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
|
||||
initialData: {
|
||||
connected: true,
|
||||
@@ -281,9 +284,9 @@ const DashboardDevices: FC = () => {
|
||||
|
||||
const customize = () => {
|
||||
if (selectedDevice == 99) {
|
||||
navigate('/settings/customentities');
|
||||
navigate('/customentities');
|
||||
} else {
|
||||
navigate('/settings/customization', { state: selectedDevice });
|
||||
navigate('/customizations', { state: selectedDevice });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -420,11 +423,8 @@ const DashboardDevices: FC = () => {
|
||||
};
|
||||
|
||||
const renderCoreData = () => (
|
||||
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
|
||||
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}>
|
||||
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
|
||||
{/* {coreData.connected && coreData.devices.length === 0 && (
|
||||
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
|
||||
)} */}
|
||||
|
||||
{coreData.connected && (
|
||||
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
|
||||
@@ -523,9 +523,11 @@ const DashboardDevices: FC = () => {
|
||||
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
<IconButton onClick={customize}>
|
||||
<FormatListNumberedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
{me.admin && (
|
||||
<IconButton onClick={customize}>
|
||||
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton onClick={handleDownloadCsv}>
|
||||
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
@@ -587,7 +589,7 @@ const DashboardDevices: FC = () => {
|
||||
<Cell>{renderNameCell(dv)}</Cell>
|
||||
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
||||
<Cell stiff>
|
||||
{dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
{me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<IconButton size="small" onClick={() => showDeviceValue(dv)}>
|
||||
{dv.v === '' && dv.c ? (
|
||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
@@ -608,7 +610,7 @@ const DashboardDevices: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.DEVICE_DATA()} titleGutter id="devices-window">
|
||||
<SectionContent id="devices-window">
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDetails()}
|
||||
@@ -619,15 +621,13 @@ const DashboardDevices: FC = () => {
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDeviceValue}
|
||||
writeable={
|
||||
me.admin &&
|
||||
selectedDeviceValue.c !== undefined &&
|
||||
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||
}
|
||||
validator={deviceValueItemValidation(selectedDeviceValue)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
<ButtonRow>
|
||||
<ButtonRow mt={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
@@ -636,4 +636,4 @@ const DashboardDevices: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardDevices;
|
||||
export default Devices;
|
||||
@@ -40,7 +40,7 @@ type DashboardDevicesDialogProps = {
|
||||
progress: boolean;
|
||||
};
|
||||
|
||||
const DashboardDevicesDialog = ({
|
||||
const DevicesDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
@@ -204,4 +204,4 @@ const DashboardDevicesDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardDevicesDialog;
|
||||
export default DevicesDialog;
|
||||
@@ -1,9 +1,19 @@
|
||||
import CommentIcon from '@mui/icons-material/CommentTwoTone';
|
||||
import EastIcon from '@mui/icons-material/East';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
|
||||
import { Box, List, ListItem, ListItemAvatar, ListItemText, Link, Typography, Button } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Link,
|
||||
Typography,
|
||||
Button,
|
||||
ListItemButton,
|
||||
Avatar
|
||||
} from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { FC } from 'react';
|
||||
@@ -39,59 +49,56 @@ const Help: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SUPPORT_INFORMATION(0)} titleGutter>
|
||||
<List>
|
||||
<SectionContent>
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MenuBookIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
{LL.HELP_INFORMATION_1()}
|
||||
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
|
||||
<Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
|
||||
{LL.CLICK_HERE()}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
<ListItemButton component="a" href="https://emsesp.github.io/docs">
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||
<MenuBookIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HELP_INFORMATION_1()} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<CommentIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
{LL.HELP_INFORMATION_2()}
|
||||
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
|
||||
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||
{LL.CLICK_HERE()}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
<ListItemButton component="a" href="https://discord.gg/3J3GgnzpyT">
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||
<CommentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HELP_INFORMATION_2()} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<GitHubIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
{LL.HELP_INFORMATION_3()}
|
||||
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
|
||||
{LL.CLICK_HERE()}
|
||||
</Link>
|
||||
<br />
|
||||
</ListItemText>
|
||||
<ListItemButton component="a" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose">
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||
<GitHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HELP_INFORMATION_3()} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Box color="warning.main">
|
||||
<Box p={2} color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.HELP_INFORMATION_4()}
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'info')}
|
||||
>
|
||||
{LL.SUPPORT_INFORMATION(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => callAPI('system', 'info')}>
|
||||
{LL.SUPPORT_INFORMATION(0)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
@@ -102,7 +109,7 @@ const Help: FC = () => {
|
||||
All Values
|
||||
</Button>
|
||||
|
||||
<Box border={1} p={1} mt={4} color="orange">
|
||||
<Box border={1} p={1} mt={4}>
|
||||
<Typography align="center" variant="subtitle1" color="orange">
|
||||
<b>{LL.HELP_INFORMATION_5()}</b>
|
||||
</Typography>
|
||||
|
||||
@@ -11,18 +11,18 @@ import { updateState, useRequest } from 'alova';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
|
||||
import SettingsSchedulerDialog from './SchedulerDialog';
|
||||
import * as EMSESP from './api';
|
||||
import { ScheduleFlag } from './types';
|
||||
import { schedulerItemValidation } from './validators';
|
||||
import type { ScheduleItem } from './types';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const SettingsScheduler: FC = () => {
|
||||
const Scheduler: FC = () => {
|
||||
const { LL, locale } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -194,6 +194,8 @@ const SettingsScheduler: FC = () => {
|
||||
</>
|
||||
);
|
||||
|
||||
useLayoutTitle(LL.SCHEDULER());
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
|
||||
@@ -249,7 +251,7 @@ const SettingsScheduler: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SCHEDULER()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
||||
@@ -268,7 +270,7 @@ const SettingsScheduler: FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
@@ -298,4 +300,4 @@ const SettingsScheduler: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsScheduler;
|
||||
export default Scheduler;
|
||||
@@ -32,7 +32,7 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
type SettingsSchedulerDialogProps = {
|
||||
type SchedulerDialogProps = {
|
||||
open: boolean;
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
@@ -42,15 +42,7 @@ type SettingsSchedulerDialogProps = {
|
||||
dow: string[];
|
||||
};
|
||||
|
||||
const SettingsSchedulerDialog = ({
|
||||
open,
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator,
|
||||
dow
|
||||
}: SettingsSchedulerDialogProps) => {
|
||||
const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -246,4 +238,4 @@ const SettingsSchedulerDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsSchedulerDialog;
|
||||
export default SchedulerDialog;
|
||||
@@ -8,26 +8,27 @@ import { useSort, SortToggleType } from '@table-library/react-table-library/sort
|
||||
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 { useRequest } from 'alova';
|
||||
import { useState, useContext, useEffect } from 'react';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DashboardSensorsAnalogDialog from './DashboardSensorsAnalogDialog';
|
||||
import DashboardSensorsTemperatureDialog from './DashboardSensorsTemperatureDialog';
|
||||
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
|
||||
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames, AnalogType } from './types';
|
||||
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
|
||||
import type { TemperatureSensor, AnalogSensor } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, SectionContent } from 'components';
|
||||
import { ButtonRow, SectionContent, useLayoutTitle } from 'components';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const DashboardSensors: FC = () => {
|
||||
const Sensors: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
|
||||
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
|
||||
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
|
||||
@@ -51,8 +52,6 @@ const DashboardSensors: FC = () => {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const isAdmin = me.admin;
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
@@ -170,6 +169,8 @@ const DashboardSensors: FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
useLayoutTitle(LL.SENSORS());
|
||||
|
||||
const formatDurationMin = (duration_min: number) => {
|
||||
const days = Math.trunc((duration_min * 60000) / 86400000);
|
||||
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
|
||||
@@ -220,7 +221,7 @@ const DashboardSensors: FC = () => {
|
||||
}
|
||||
|
||||
const updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||
if (isAdmin) {
|
||||
if (me.admin) {
|
||||
setSelectedTemperatureSensor(ts);
|
||||
setTemperatureDialogOpen(true);
|
||||
}
|
||||
@@ -246,7 +247,7 @@ const DashboardSensors: FC = () => {
|
||||
};
|
||||
|
||||
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||
if (isAdmin) {
|
||||
if (me.admin) {
|
||||
setCreating(false);
|
||||
setSelectedAnalogSensor(as);
|
||||
setAnalogDialogOpen(true);
|
||||
@@ -406,25 +407,20 @@ const DashboardSensors: FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SENSOR_DATA()} titleGutter>
|
||||
{sensorData.ts.length > 0 && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<RenderTemperatureSensors />
|
||||
{selectedTemperatureSensor && (
|
||||
<DashboardSensorsTemperatureDialog
|
||||
open={temperatureDialogOpen}
|
||||
onClose={onTemperatureDialogClose}
|
||||
onSave={onTemperatureDialogSave}
|
||||
selectedItem={selectedTemperatureSensor}
|
||||
validator={temperatureSensorItemValidation()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<SectionContent>
|
||||
<Typography sx={{ pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<RenderTemperatureSensors />
|
||||
{selectedTemperatureSensor && (
|
||||
<DashboardSensorsTemperatureDialog
|
||||
open={temperatureDialogOpen}
|
||||
onClose={onTemperatureDialogClose}
|
||||
onSave={onTemperatureDialogSave}
|
||||
selectedItem={selectedTemperatureSensor}
|
||||
validator={temperatureSensorItemValidation()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sensorData?.analog_enabled === true && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
|
||||
@@ -443,15 +439,14 @@ const DashboardSensors: FC = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ButtonRow>
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
{sensorData?.analog_enabled === true && (
|
||||
{sensorData?.analog_enabled === true && me.admin && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@@ -467,4 +462,4 @@ const DashboardSensors: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensors;
|
||||
export default Sensors;
|
||||
@@ -38,7 +38,7 @@ type DashboardSensorsAnalogDialogProps = {
|
||||
validator: Schema;
|
||||
};
|
||||
|
||||
const DashboardSensorsAnalogDialog = ({
|
||||
const SensorsAnalogDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
@@ -296,4 +296,4 @@ const DashboardSensorsAnalogDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensorsAnalogDialog;
|
||||
export default SensorsAnalogDialog;
|
||||
@@ -26,7 +26,7 @@ import { numberValue, updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
|
||||
type DashboardSensorsTemperatureDialogProps = {
|
||||
type SensorsTemperatureDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ts: TemperatureSensor) => void;
|
||||
@@ -34,13 +34,13 @@ type DashboardSensorsTemperatureDialogProps = {
|
||||
validator: Schema;
|
||||
};
|
||||
|
||||
const DashboardSensorsTemperatureDialog = ({
|
||||
const SensorsTemperatureDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator
|
||||
}: DashboardSensorsTemperatureDialogProps) => {
|
||||
}: SensorsTemperatureDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
||||
@@ -119,4 +119,4 @@ const DashboardSensorsTemperatureDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensorsTemperatureDialog;
|
||||
export default SensorsTemperatureDialog;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import SettingsApplication from './SettingsApplication';
|
||||
import SettingsCustomEntities from './SettingsCustomEntities';
|
||||
import SettingsCustomization from './SettingsCustomization';
|
||||
import SettingsScheduler from './SettingsScheduler';
|
||||
import type { FC } from 'react';
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Settings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
useLayoutTitle(LL.SETTINGS_OF(''));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/settings/application" label={LL.APPLICATION_SETTINGS()} />
|
||||
<Tab value="/settings/customization" label={LL.CUSTOMIZATIONS()} />
|
||||
<Tab value="/settings/scheduler" label={LL.SCHEDULER()} />
|
||||
<Tab value="/settings/customentities" label={LL.CUSTOM_ENTITIES(0)} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="application" element={<SettingsApplication />} />
|
||||
<Route path="customization" element={<SettingsCustomization />} />
|
||||
<Route path="scheduler" element={<SettingsScheduler />} />
|
||||
<Route path="customentities" element={<SettingsCustomEntities />} />
|
||||
<Route path="*" element={<Navigate replace to="/settings/application" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
130
interface/src/project/SystemActivity.tsx
Normal file
130
interface/src/project/SystemActivity.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { Button } from '@mui/material';
|
||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import type { Stat } from './types';
|
||||
|
||||
import type { Translation } from 'i18n/i18n-types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const SystemActivity: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.ACTIVITY());
|
||||
|
||||
const stats_theme = tableTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
|
||||
.th {
|
||||
height: 36px;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
&:nth-of-type(even) .td {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:not(:first-of-type) {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => loadData(), 30000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
|
||||
const showName = (id: any) => {
|
||||
const name: keyof Translation['STATUS_NAMES'] = id;
|
||||
return LL.STATUS_NAMES[name]();
|
||||
};
|
||||
|
||||
const showQuality = (stat: Stat) => {
|
||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||
return;
|
||||
}
|
||||
if (stat.q === 100) {
|
||||
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
|
||||
}
|
||||
if (stat.q >= 95) {
|
||||
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
|
||||
} else {
|
||||
return <div style={{ color: 'red' }}>{stat.q}%</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize />
|
||||
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((stat: Stat) => (
|
||||
<Row key={stat.id} item={stat}>
|
||||
<Cell>{showName(stat.id)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
|
||||
<Cell stiff>{showQuality(stat)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
<ButtonRow mt={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default SystemActivity;
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
APIcall,
|
||||
Settings,
|
||||
Status,
|
||||
Activity,
|
||||
CoreData,
|
||||
Devices,
|
||||
DeviceEntity,
|
||||
@@ -25,7 +25,7 @@ export const readDeviceData = (id: number) =>
|
||||
});
|
||||
export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data);
|
||||
|
||||
// SettingsApplication
|
||||
// Application Settings
|
||||
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
|
||||
export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data);
|
||||
export const getBoardProfile = (boardProfile: string) =>
|
||||
@@ -33,17 +33,18 @@ export const getBoardProfile = (boardProfile: string) =>
|
||||
params: { boardProfile }
|
||||
});
|
||||
|
||||
// DashboardSensors
|
||||
// Sensors
|
||||
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
|
||||
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
|
||||
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
|
||||
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
|
||||
|
||||
// DashboardStatus
|
||||
export const readStatus = () => alovaInstance.Get<Status>('/rest/status');
|
||||
// Activity
|
||||
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
|
||||
|
||||
export const scanDevices = () => alovaInstance.Post('/rest/scanDevices');
|
||||
|
||||
// HelpInformation
|
||||
// API, used in HelpInformation
|
||||
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
|
||||
|
||||
// UploadFileForm
|
||||
|
||||
@@ -50,13 +50,7 @@ export interface Stat {
|
||||
q: number; // quality
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
status: busConnectionStatus;
|
||||
tx_mode: number;
|
||||
uptime: number;
|
||||
num_devices: number;
|
||||
num_sensors: number;
|
||||
num_analogs: number;
|
||||
export interface Activity {
|
||||
stats: Stat[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user