mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 08:49:52 +03:00
refactor web file structure and seperate settings from status
This commit is contained in:
127
interface/src/app/status/APStatus.tsx
Normal file
127
interface/src/app/status/APStatus.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import * as APApi from 'api/ap';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { APStatusType } from 'types';
|
||||
import { APNetworkStatus } from 'types';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return theme.palette.info.main;
|
||||
case APNetworkStatus.LINGERING:
|
||||
return theme.palette.warning.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const APStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF(LL.ACCESS_POINT(0)));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const apStatus = ({ status }: APStatusType) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case APNetworkStatus.LINGERING:
|
||||
return 'Lingering until idle';
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: apStatusHighlight(data, theme) }}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={apStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.ADDRESS_OF('IP')}
|
||||
secondary={data.ip_address}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.ADDRESS_OF('MAC')}
|
||||
secondary={data.mac_address}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ComputerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.AP_CLIENTS()} secondary={data.station_num} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default APStatus;
|
||||
148
interface/src/app/status/Activity.tsx
Normal file
148
interface/src/app/status/Activity.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
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 { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { Translation } from 'i18n/i18n-types';
|
||||
|
||||
import * as EMSESP from '../main/api';
|
||||
import type { Stat } from '../main/types';
|
||||
|
||||
const SystemActivity: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.EMS_BUS_STATUS());
|
||||
|
||||
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: number) => {
|
||||
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: Stat[]) => (
|
||||
<>
|
||||
<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;
|
||||
221
interface/src/app/status/HardwareStatus.tsx
Normal file
221
interface/src/app/status/HardwareStatus.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
|
||||
import DevicesIcon from '@mui/icons-material/Devices';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
|
||||
import SdStorageIcon from '@mui/icons-material/SdStorage';
|
||||
import TapAndPlayIcon from '@mui/icons-material/TapAndPlay';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import BBQKeesIcon from './bbqkees.svg';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
const ESPSystemStatus: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.STATUS_OF(LL.HARDWARE()));
|
||||
|
||||
const {
|
||||
data: data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useRequest(SystemApi.readESPSystemStatus, { force: true });
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#003289', color: 'white' }}>
|
||||
{data.model ? (
|
||||
<img
|
||||
src={BBQKeesIcon}
|
||||
style={{ width: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
) : (
|
||||
<TapAndPlayIcon />
|
||||
)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.HARDWARE() + ' ' + LL.DEVICE()}
|
||||
secondary={data.model ? data.model : data.cpu_type}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<DevicesIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="SDK"
|
||||
secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<DeveloperBoardIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="CPU"
|
||||
secondary={
|
||||
data.esp_platform +
|
||||
'/' +
|
||||
data.cpu_type +
|
||||
' (rev.' +
|
||||
data.cpu_rev +
|
||||
', ' +
|
||||
(data.cpu_cores == 1 ? 'single-core)' : 'dual-core)') +
|
||||
' @ ' +
|
||||
data.cpu_freq_mhz +
|
||||
' Mhz'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<MemoryIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.FREE_MEMORY()}
|
||||
secondary={
|
||||
formatNumber(data.free_heap) +
|
||||
' KB (' +
|
||||
formatNumber(data.max_alloc_heap) +
|
||||
' KB max alloc)'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
||||
<>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<AppsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.PSRAM()}
|
||||
secondary={
|
||||
formatNumber(data.psram_size) +
|
||||
' KB / ' +
|
||||
formatNumber(data.free_psram) +
|
||||
' KB'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<SdStorageIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.FLASH()}
|
||||
secondary={
|
||||
formatNumber(data.flash_chip_size) +
|
||||
' KB , ' +
|
||||
(data.flash_chip_speed / 1000000).toFixed(0) +
|
||||
' MHz'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<SdCardAlertIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.APPSIZE()}
|
||||
secondary={
|
||||
data.partition +
|
||||
': ' +
|
||||
formatNumber(data.app_used) +
|
||||
' KB / ' +
|
||||
formatNumber(data.app_free) +
|
||||
' KB'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.FILESYSTEM()}
|
||||
secondary={
|
||||
formatNumber(data.fs_used) +
|
||||
' KB / ' +
|
||||
formatNumber(data.fs_free) +
|
||||
' KB'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default ESPSystemStatus;
|
||||
181
interface/src/app/status/MqttStatus.tsx
Normal file
181
interface/src/app/status/MqttStatus.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ReportIcon from '@mui/icons-material/Report';
|
||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { MqttStatusType } from 'types';
|
||||
import { MqttDisconnectReason } from 'types';
|
||||
|
||||
export const mqttStatusHighlight = (
|
||||
{ enabled, connected }: MqttStatusType,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (!enabled) {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
if (connected) {
|
||||
return theme.palette.success.main;
|
||||
}
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttPublishHighlight = (
|
||||
{ mqtt_fails }: MqttStatusType,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttQueueHighlight = (
|
||||
{ mqtt_queued }: MqttStatusType,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (mqtt_queued <= 1) return theme.palette.success.main;
|
||||
|
||||
return theme.palette.warning.main;
|
||||
};
|
||||
|
||||
const MqttStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF('MQTT'));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatusType) => {
|
||||
if (!enabled) {
|
||||
return LL.NOT_ENABLED();
|
||||
}
|
||||
if (connected) {
|
||||
return LL.CONNECTED(0) + (connect_count > 1 ? ' (' + connect_count + ')' : '');
|
||||
}
|
||||
return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
|
||||
};
|
||||
|
||||
const disconnectReason = ({ disconnect_reason }: MqttStatusType) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return 'TCP disconnected';
|
||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
return 'Unacceptable protocol version';
|
||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||
return 'Client ID rejected';
|
||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||
return 'Server unavailable';
|
||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||
return 'Malformed credentials';
|
||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||
return 'Not authorized';
|
||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||
return 'TLS fingerprint invalid';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
const renderConnectionStatus = () => (
|
||||
<>
|
||||
{!data.connected && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ReportIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.DISCONNECT_REASON()}
|
||||
secondary={disconnectReason(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
)}
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ID_OF(LL.CLIENT())} secondary={data.client_id} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttQueueHighlight(data, theme) }}>
|
||||
<AutoAwesomeMotionIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.MQTT_QUEUE()} secondary={data.mqtt_queued} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttPublishHighlight(data, theme) }}>
|
||||
<SpeakerNotesOffIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ERRORS_OF('MQTT')} secondary={data.mqtt_fails} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttStatusHighlight(data, theme) }}>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{data.enabled && renderConnectionStatus()}
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default MqttStatus;
|
||||
253
interface/src/app/status/NTPStatus.tsx
Normal file
253
interface/src/app/status/NTPStatus.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
||||
import UpdateIcon from '@mui/icons-material/Update';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import * as NTPApi from 'api/ntp';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NTPStatusType, Time } from 'types';
|
||||
import { NTPSyncStatus } from 'types';
|
||||
import { formatDateTime, formatLocalDateTime } from 'utils';
|
||||
|
||||
const NTPStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
|
||||
|
||||
const [localTime, setLocalTime] = useState<string>('');
|
||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF('NTP'));
|
||||
|
||||
const { send: updateTime } = useRequest(
|
||||
(local_time: Time) => NTPApi.updateTime(local_time),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
NTPApi.updateTime;
|
||||
|
||||
const isNtpActive = ({ status }: NTPStatusType) =>
|
||||
status === NTPSyncStatus.NTP_ACTIVE;
|
||||
const isNtpEnabled = ({ status }: NTPStatusType) =>
|
||||
status !== NTPSyncStatus.NTP_DISABLED;
|
||||
|
||||
const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return theme.palette.info.main;
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return theme.palette.error.main;
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setLocalTime(event.target.value);
|
||||
|
||||
const openSetTime = () => {
|
||||
setLocalTime(formatLocalDateTime(new Date()));
|
||||
setSettingTime(true);
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const ntpStatus = ({ status }: NTPStatusType) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return LL.NOT_ENABLED();
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const configureTime = async () => {
|
||||
setProcessing(true);
|
||||
|
||||
await updateTime({ local_time: formatLocalDateTime(new Date(localTime)) })
|
||||
.then(async () => {
|
||||
toast.success(LL.TIME_SET());
|
||||
setSettingTime(false);
|
||||
await loadData();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
})
|
||||
.finally(() => {
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderSetTimeDialog = () => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={settingTime}
|
||||
onClose={() => setSettingTime(false)}
|
||||
>
|
||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label={LL.LOCAL_TIME(0)}
|
||||
type="datetime-local"
|
||||
value={localTime}
|
||||
onChange={updateLocalTime}
|
||||
disabled={processing}
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setSettingTime(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<AccessTimeIcon />}
|
||||
variant="outlined"
|
||||
onClick={configureTime}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: ntpStatusHighlight(data, theme) }}>
|
||||
<UpdateIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={ntpStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{isNtpEnabled(data) && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DnsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.NTP_SERVER()} secondary={data.server} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
)}
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<AccessTimeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.LOCAL_TIME(0)}
|
||||
secondary={formatDateTime(data.local_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<SwapVerticalCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.UTC_TIME()}
|
||||
secondary={formatDateTime(data.utc_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
{data && !isNtpActive(data) && (
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button
|
||||
onClick={openSetTime}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<AccessTimeIcon />}
|
||||
>
|
||||
{LL.SET_TIME(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{renderSetTimeDialog()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NTPStatus;
|
||||
239
interface/src/app/status/NetworkStatus.tsx
Normal file
239
interface/src/app/status/NetworkStatus.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import GiteIcon from '@mui/icons-material/Gite';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import RouterIcon from '@mui/icons-material/Router';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import * as NetworkApi from 'api/network';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NetworkStatusType } from 'types';
|
||||
import { NetworkConnectionStatus } from 'types';
|
||||
|
||||
const isConnected = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
const networkStatusHighlight = ({ status }: NetworkStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return theme.palette.info.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
|
||||
if (rssi <= -85) {
|
||||
return theme.palette.error.main;
|
||||
} else if (rssi <= -75) {
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
return theme.palette.success.main;
|
||||
};
|
||||
|
||||
export const isWiFi = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||
export const isEthernet = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatusType) => {
|
||||
if (!dns_ip_1) {
|
||||
return 'none';
|
||||
}
|
||||
return dns_ip_1 + (!dns_ip_2 || dns_ip_2 === '0.0.0.0' ? '' : ',' + dns_ip_2);
|
||||
};
|
||||
|
||||
const IPs = (status: NetworkStatusType) => {
|
||||
if (
|
||||
!status.local_ipv6 ||
|
||||
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
|
||||
) {
|
||||
return status.local_ip;
|
||||
}
|
||||
if (!status.local_ip || status.local_ip === '0.0.0.0') {
|
||||
return status.local_ipv6;
|
||||
}
|
||||
return status.local_ip + ', ' + status.local_ipv6;
|
||||
};
|
||||
|
||||
const NetworkStatus: FC = () => {
|
||||
const {
|
||||
data: data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useRequest(NetworkApi.readNetworkStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF(LL.NETWORK(1)));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const networkStatus = ({ status }: NetworkStatusType) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return LL.IDLE();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (WiFi)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
||||
{isWiFi(data) && <WifiIcon />}
|
||||
{isEthernet(data) && <RouterIcon />}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
||||
<GiteIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HOSTNAME()} secondary={data.hostname} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{isWiFi(data) && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkQualityHighlight(data, theme) }}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="SSID (RSSI)"
|
||||
secondary={data.ssid + ' (' + data.rssi + ' dBm)'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
)}
|
||||
{isConnected(data) && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={IPs(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.ADDRESS_OF('MAC')}
|
||||
secondary={data.mac_address}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.NETWORK_SUBNET()}
|
||||
secondary={data.subnet_mask}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<SettingsInputComponentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.NETWORK_GATEWAY()}
|
||||
secondary={data.gateway_ip || 'none'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DnsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.NETWORK_DNS()}
|
||||
secondary={dnsServers(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NetworkStatus;
|
||||
47
interface/src/app/status/RestartMonitor.tsx
Normal file
47
interface/src/app/status/RestartMonitor.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { FormLoader } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const RESTART_TIMEOUT = 2 * 60 * 1000;
|
||||
const POLL_INTERVAL = 3000;
|
||||
|
||||
const RestartMonitor: FC = () => {
|
||||
const [failed, setFailed] = useState<boolean>(false);
|
||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
||||
const { LL } = useI18nContext();
|
||||
const { send } = useRequest(SystemApi.readSystemStatus, { force: true });
|
||||
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
|
||||
|
||||
const poll = useRef(async () => {
|
||||
try {
|
||||
await send();
|
||||
document.location.href = '/';
|
||||
} catch {
|
||||
if (new Date().getTime() < timeoutAt.current) {
|
||||
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void poll.current();
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
|
||||
|
||||
return (
|
||||
<FormLoader
|
||||
message={LL.APPLICATION_RESTARTING() + '...'}
|
||||
errorMessage={failed ? 'Timed out' : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestartMonitor;
|
||||
493
interface/src/app/status/Status.tsx
Normal file
493
interface/src/app/status/Status.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
import { type FC, useContext, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
||||
import LogoDevIcon from '@mui/icons-material/LogoDev';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import RouterIcon from '@mui/icons-material/Router';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import TimerIcon from '@mui/icons-material/Timer';
|
||||
import UpgradeIcon from '@mui/icons-material/Upgrade';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import * as EMSESP from 'app/main/api';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova';
|
||||
import { busConnectionStatus } from 'app/main/types';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { NTPSyncStatus, NetworkConnectionStatus } from 'types';
|
||||
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
|
||||
const SystemStatus: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useLayoutTitle(LL.STATUS_OF(''));
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [confirmScan, setConfirmScan] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const {
|
||||
data: data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useRequest(SystemApi.readSystemStatus, { force: true });
|
||||
|
||||
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
const busStatus = () => {
|
||||
if (data) {
|
||||
switch (data.status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (' + formatDurationSec(data.bus_uptime) + ')';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return LL.TX_ISSUES();
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return LL.DISCONNECTED();
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
const busStatusHighlight = () => {
|
||||
switch (data.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 ntpStatus = () => {
|
||||
switch (data.ntp_status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return LL.NOT_ENABLED();
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const ntpStatusHighlight = () => {
|
||||
switch (data.ntp_status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return theme.palette.info.main;
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return theme.palette.error.main;
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
const networkStatusHighlight = () => {
|
||||
switch (data.network_status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return theme.palette.info.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const networkStatus = () => {
|
||||
switch (data.network_status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return LL.IDLE();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (WiFi, ' + data.wifi_rssi + ' dBm)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const activeHighlight = (value: boolean) =>
|
||||
value ? theme.palette.success.main : theme.palette.info.main;
|
||||
|
||||
const scan = async () => {
|
||||
await scanDevices()
|
||||
.then(() => {
|
||||
toast.info(LL.SCANNING() + '...');
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.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 restart = async () => {
|
||||
setProcessing(true);
|
||||
await restartCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const partition = async () => {
|
||||
setProcessing(true);
|
||||
await partitionCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderRestartDialog = () => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmRestart}
|
||||
onClose={() => setConfirmRestart(false)}
|
||||
>
|
||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmRestart(false)}
|
||||
disabled={processing}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={partition}
|
||||
disabled={processing}
|
||||
color="warning"
|
||||
>
|
||||
EMS-ESP Loader
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={restart}
|
||||
disabled={processing}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
|
||||
<TimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.UPTIME()}
|
||||
secondary={formatDurationSec(data.uptime)}
|
||||
/>
|
||||
{me.admin && (
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setConfirmRestart(true)}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={LogoDevIcon}
|
||||
bgcolor="#40828f"
|
||||
label={LL.LOG_OF(LL.SYSTEM(0))}
|
||||
text={data.emsesp_version}
|
||||
to="/status/log"
|
||||
/>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5d89f7', color: 'white' }}>
|
||||
<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 })
|
||||
}
|
||||
/>
|
||||
{me.admin && (
|
||||
<Button
|
||||
startIcon={<PermScanWifiIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => setConfirmScan(true)}
|
||||
>
|
||||
{LL.SCAN_DEVICES()}
|
||||
</Button>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#134ba2', color: 'white' }}>
|
||||
<BuildIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.EMS_ESP_VER()}
|
||||
secondary={data.emsesp_version}
|
||||
/>
|
||||
{me.admin && (
|
||||
<Button
|
||||
startIcon={<UpgradeIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => navigate('/settings/upload')}
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={DirectionsBusIcon}
|
||||
bgcolor={busStatusHighlight()}
|
||||
label={LL.EMS_BUS_STATUS()}
|
||||
text={busStatus()}
|
||||
to="/status/activity"
|
||||
/>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={MemoryIcon}
|
||||
bgcolor="#68374d"
|
||||
label={LL.STATUS_OF(LL.HARDWARE())}
|
||||
text={formatNumber(data.free_heap) + ' KB' + ' ' + LL.FREE_MEMORY()}
|
||||
to="/status/hardwarestatus"
|
||||
/>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={
|
||||
data.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
||||
? WifiIcon
|
||||
: RouterIcon
|
||||
}
|
||||
bgcolor={networkStatusHighlight()}
|
||||
label={LL.STATUS_OF(LL.NETWORK(1))}
|
||||
text={networkStatus()}
|
||||
to="/status/network"
|
||||
/>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={DeviceHubIcon}
|
||||
bgcolor={activeHighlight(data.mqtt_status)}
|
||||
label={LL.STATUS_OF('MQTT')}
|
||||
text={data.mqtt_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||
to="/status/mqtt"
|
||||
/>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor={ntpStatusHighlight()}
|
||||
label={LL.STATUS_OF('NTP')}
|
||||
text={ntpStatus()}
|
||||
to="/status/ntp"
|
||||
/>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor={activeHighlight(data.ap_status)}
|
||||
label={LL.STATUS_OF(LL.ACCESS_POINT(0))}
|
||||
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||
to="/status/ap"
|
||||
/>
|
||||
</List>
|
||||
|
||||
{renderScanDialog()}
|
||||
{renderRestartDialog()}
|
||||
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
299
interface/src/app/status/SystemLog.tsx
Normal file
299
interface/src/app/status/SystemLog.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Grid,
|
||||
MenuItem,
|
||||
TextField,
|
||||
styled
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { fetchLogES } from 'api/system';
|
||||
|
||||
import { useSSE } from '@alova/scene-react';
|
||||
import { useRequest } from 'alova';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { LogEntry, LogSettings } from 'types';
|
||||
import { LogLevel } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
|
||||
const ButtonTextColors = {
|
||||
[LogLevel.ERROR]: '#ff0000', // red
|
||||
[LogLevel.WARNING]: '#ffcc00', // yellow
|
||||
[LogLevel.NOTICE]: '#ffffff', // white
|
||||
[LogLevel.INFO]: '#ffffff', // yellow
|
||||
[LogLevel.DEBUG]: '#00ffff', // cyan
|
||||
[LogLevel.TRACE]: '#00ffff' // cyan
|
||||
};
|
||||
|
||||
const LogEntryLine = styled('div')(
|
||||
({ details: { level } }: { details: { level: LogLevel } }) => ({
|
||||
color: ButtonTextColors[level],
|
||||
font: '14px monospace',
|
||||
whiteSpace: 'nowrap'
|
||||
})
|
||||
);
|
||||
|
||||
const topOffset = () =>
|
||||
document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
|
||||
const leftOffset = () =>
|
||||
document.getElementById('log-window')?.getBoundingClientRect().left || 0;
|
||||
|
||||
const levelLabel = (level: LogLevel) => {
|
||||
switch (level) {
|
||||
case LogLevel.ERROR:
|
||||
return 'ERROR';
|
||||
case LogLevel.WARNING:
|
||||
return 'WARNING';
|
||||
case LogLevel.NOTICE:
|
||||
return 'NOTICE';
|
||||
case LogLevel.INFO:
|
||||
return 'INFO';
|
||||
case LogLevel.DEBUG:
|
||||
return 'DEBUG';
|
||||
case LogLevel.TRACE:
|
||||
return 'TRACE';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const SystemLog: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.LOG_OF(LL.SYSTEM(0)));
|
||||
|
||||
const {
|
||||
loadData,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<LogSettings>({
|
||||
read: SystemApi.readLogSettings,
|
||||
update: SystemApi.updateLogSettings
|
||||
});
|
||||
|
||||
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
||||
const [lastIndex, setLastIndex] = useState<number>(0);
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const { onMessage, onError } = useSSE(fetchLogES, {
|
||||
immediate: true,
|
||||
// withCredentials: true,
|
||||
interceptByGlobalResponded: false
|
||||
});
|
||||
|
||||
onMessage((message: { id: number; data: string }) => {
|
||||
const rawData = message.data;
|
||||
const logentry = JSON.parse(rawData) as LogEntry;
|
||||
if (logentry.i > lastIndex) {
|
||||
setLastIndex(logentry.i);
|
||||
setLogEntries((log) => [...log, logentry]);
|
||||
}
|
||||
});
|
||||
|
||||
onError(() => {
|
||||
toast.error('No connection to Log server');
|
||||
});
|
||||
|
||||
// called on page load to reset pointer and fetch all log entries
|
||||
useRequest(SystemApi.fetchLog());
|
||||
|
||||
const paddedLevelLabel = (level: LogLevel) => {
|
||||
const label = levelLabel(level);
|
||||
return data?.compact ? ' ' + label[0] : label.padStart(8, '\xa0');
|
||||
};
|
||||
|
||||
const paddedNameLabel = (name: string) => {
|
||||
const label = '[' + name + ']';
|
||||
return data?.compact ? label : label.padEnd(12, '\xa0');
|
||||
};
|
||||
|
||||
const paddedIDLabel = (id: number) => {
|
||||
const label = id + ':';
|
||||
return data?.compact ? label : label.padEnd(7, '\xa0');
|
||||
};
|
||||
|
||||
const onDownload = () => {
|
||||
let result = '';
|
||||
for (const i of logEntries) {
|
||||
result +=
|
||||
i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(result)
|
||||
);
|
||||
a.setAttribute('download', 'log.txt');
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
await saveData();
|
||||
};
|
||||
|
||||
// handle scrolling
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (logEntries.length) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end'
|
||||
});
|
||||
}
|
||||
}, [logEntries.length]);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="level"
|
||||
label={LL.LOG_LEVEL()}
|
||||
value={data.level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
<MenuItem value={3}>ERROR</MenuItem>
|
||||
<MenuItem value={4}>WARNING</MenuItem>
|
||||
<MenuItem value={5}>NOTICE</MenuItem>
|
||||
<MenuItem value={6}>INFO</MenuItem>
|
||||
<MenuItem value={9}>ALL</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="max_messages"
|
||||
label={LL.BUFFER_SIZE()}
|
||||
value={data.max_messages}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={25}>25</MenuItem>
|
||||
<MenuItem value={50}>50</MenuItem>
|
||||
<MenuItem value={75}>75</MenuItem>
|
||||
<MenuItem value={100}>100</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.compact}
|
||||
onChange={updateFormValue}
|
||||
name="compact"
|
||||
/>
|
||||
}
|
||||
label={LL.COMPACT()}
|
||||
/>
|
||||
</Grid>
|
||||
<Box
|
||||
sx={{
|
||||
'& button, & a, & .MuiCard-root': {
|
||||
ml: 3
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={onDownload}
|
||||
>
|
||||
{LL.EXPORT()}
|
||||
</Button>
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
color="info"
|
||||
onClick={saveSettings}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'black',
|
||||
overflowY: 'scroll',
|
||||
position: 'absolute',
|
||||
right: 18,
|
||||
bottom: 18,
|
||||
left: () => leftOffset(),
|
||||
top: () => topOffset(),
|
||||
p: 1
|
||||
}}
|
||||
>
|
||||
{logEntries.map((e) => (
|
||||
<LogEntryLine details={{ level: e.l }} key={e.i}>
|
||||
<span>{e.t}</span>
|
||||
<span>{paddedLevelLabel(e.l)} </span>
|
||||
<span>{paddedIDLabel(e.i)} </span>
|
||||
<span>{paddedNameLabel(e.n)} </span>
|
||||
<span>{e.m}</span>
|
||||
</LogEntryLine>
|
||||
))}
|
||||
<div ref={ref} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent id="log-window">
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemLog;
|
||||
26
interface/src/app/status/bbqkees.svg
Normal file
26
interface/src/app/status/bbqkees.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg width="20mm" height="30.146mm" version="1.1" viewBox="0 0 20 30.146" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-86.195 -104.7)">
|
||||
<g transform="matrix(.53414 0 0 .53414 70.913 49.568)">
|
||||
<g fill="#fff">
|
||||
<path d="m66.034 122.7h-37.398a18.722 18.722 0 0 0-0.02643 0.52631 18.722 18.722 0 0 0 18.722 18.722 18.722 18.722 0 0 0 18.722-18.722 18.722 18.722 0 0 0-0.01882-0.52631z" color="#000000" stroke-width=".15037" style="-inkscape-stroke:none"/>
|
||||
<path d="m30.943 141.6v2h32.174v-2z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m40.738 142-6.9004 11.951 1.7305 1 6.9023-11.951z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<g fill="#fff" fill-rule="evenodd">
|
||||
<path d="m32.395 158.45-0.0041-7.9929 6.9282 4z" color="#000000" stroke-width=".8pt" style="-inkscape-stroke:none"/>
|
||||
<path d="m31.857 149.54 0.0039 9.8398 0.80078-0.46094 7.7246-4.4551zm1.0664 1.8457 5.3281 3.0762-5.3242 3.0723z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
</g>
|
||||
<path d="m56.547 103.22-1.5645 1.2461s0.33514 0.43498 0.48828 1.0391c0.15315 0.60408 0.18926 1.209-0.57617 1.9688-1.1965 1.1876-1.5061 2.5227-1.4004 3.4902 0.10574 0.96749 0.5918 1.6426 0.5918 1.6426l1.6211-1.1699s-0.17552-0.2403-0.22461-0.68946c-0.04909-0.44915-0.0062-1.0331 0.82031-1.8535 1.2576-1.2483 1.3774-2.8142 1.1074-3.8789s-0.86328-1.7949-0.86328-1.7949z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m48.918 103.22-1.5645 1.2461s0.33513 0.43498 0.48828 1.0391 0.18926 1.209-0.57617 1.9688c-1.1965 1.1876-1.5042 2.5227-1.3984 3.4902 0.10574 0.96749 0.58984 1.6426 0.58984 1.6426l1.623-1.1699s-0.17552-0.2403-0.22461-0.68946c-0.04909-0.44915-0.0062-1.0331 0.82031-1.8535 1.2576-1.2483 1.3754-2.8142 1.1055-3.8789s-0.86328-1.7949-0.86328-1.7949z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m41.291 103.22-1.5664 1.2461s0.33709 0.43498 0.49024 1.0391 0.18926 1.209-0.57617 1.9688c-1.1965 1.1876-1.5061 2.5227-1.4004 3.4902s0.5918 1.6426 0.5918 1.6426l1.6211-1.1699s-0.17552-0.2403-0.22461-0.68946c-0.04909-0.44916-0.0062-1.0331 0.82031-1.8535 1.2576-1.2483 1.3754-2.8142 1.1055-3.8789-0.26993-1.0647-0.86133-1.7949-0.86133-1.7949z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m54.896 113.47c-1.4548 0-2.706 0.87699-3.5293 2.0957-0.82334 1.2187-1.291 2.8147-1.291 4.5586v8e-3c5.87e-4 0.0787 0.0036 0.15349 0.0059 0.22656l1.998-0.0605c-0.0019-0.0604-0.0034-0.11856-0.0039-0.17383v-2e-3c-1.6e-5 -2e-3 1.4e-5 -4e-3 0-6e-3 0.0015-1.3755 0.38921-2.6056 0.94726-3.4316 0.55917-0.82768 1.2182-1.2148 1.873-1.2148 0.65485 0 1.3139 0.38716 1.873 1.2148s0.94727 2.0607 0.94727 3.4394h2c0-1.7439-0.46768-3.3399-1.291-4.5586-0.82334-1.2187-2.0745-2.0957-3.5293-2.0957z" color="#000000" style="-inkscape-stroke:none;paint-order:stroke fill markers"/>
|
||||
<path d="m47.27 113.47c-1.4548 0-2.706 0.87699-3.5293 2.0957-0.82334 1.2187-1.291 2.8147-1.291 4.5586h2c0-1.3788 0.38809-2.6118 0.94726-3.4394 0.55917-0.82768 1.2182-1.2148 1.873-1.2148 0.65484-1e-5 1.3119 0.38716 1.8711 1.2148 0.55802 0.82598 0.94567 2.0563 0.94727 3.4316-0.0018 0.0584-0.0035 0.11387-0.0059 0.16406l1.998 0.0957c0.0037-0.0788 0.0057-0.15249 0.0078-0.22071l2e-3 -0.0156v-0.0156c0-1.7439-0.46767-3.3399-1.291-4.5586-0.82334-1.2187-2.0745-2.0957-3.5293-2.0957z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m39.641 113.47c-1.4548 0-2.706 0.87699-3.5293 2.0957-0.82334 1.2187-1.291 2.8147-1.291 4.5586v0.012c8.96e-4 0.0721 0.0037 0.15312 0.0078 0.24024l1.9961-0.0957c-0.0021-0.0456-0.0031-0.0976-0.0039-0.15625v-2e-3c3.87e-4 -1.378 0.38837-2.6102 0.94727-3.4375 0.55917-0.82768 1.2182-1.2148 1.873-1.2148 0.65484 0 1.3139 0.38716 1.873 1.2148s0.94726 2.0607 0.94726 3.4394h2c0-1.7439-0.46768-3.3399-1.291-4.5586s-2.0745-2.0957-3.5293-2.0957z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m57.557 119.33v2h5.8418v-2z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
<path d="m31.137 119.33v2h5.8418v-2z" color="#000000" style="-inkscape-stroke:none"/>
|
||||
</g>
|
||||
<path d="m35.826 119.95v0.77149h22.883v-0.77149z" color="#000000" fill-opacity="0" style="-inkscape-stroke:none"/>
|
||||
<path d="m59.064 150.73c-2.4534 0-4.4629 2.0115-4.4629 4.4648s2.0095 4.4629 4.4629 4.4629 4.4629-2.0095 4.4629-4.4629-2.0095-4.4648-4.4629-4.4648zm0 2c1.3725 0 2.4629 1.0924 2.4629 2.4648s-1.0904 2.4629-2.4629 2.4629-2.4629-1.0904-2.4629-2.4629 1.0904-2.4648 2.4629-2.4648z" color="#000000" fill="#fff" style="-inkscape-stroke:none;paint-order:stroke fill markers"/>
|
||||
<path d="m54.932 142.14-1.8359 0.79687 3.752 8.6582 1.8359-0.79493z" color="#000000" fill="#fff" style="-inkscape-stroke:none"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
Reference in New Issue
Block a user