mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-13 11:19:52 +03:00
optimizations
This commit is contained in:
@@ -34,37 +34,43 @@ export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getApStatusText = (
|
||||
status: APNetworkStatus,
|
||||
LL: ReturnType<typeof useI18nContext>['LL']
|
||||
) => {
|
||||
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 APStatus = () => {
|
||||
const { data, send: loadData, error } = useRequest(APApi.readAPStatus);
|
||||
const { LL } = useI18nContext();
|
||||
const theme = useTheme();
|
||||
|
||||
useLayoutTitle(LL.ACCESS_POINT(0));
|
||||
|
||||
useInterval(() => {
|
||||
void loadData();
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(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 || ''} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<SectionContent>
|
||||
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
@@ -72,19 +78,26 @@ const APStatus = () => {
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={apStatus(data)} />
|
||||
<ListItemText
|
||||
primary={LL.STATUS_OF('')}
|
||||
secondary={getApStatusText(data.status, LL)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} />
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
@@ -93,21 +106,22 @@ const APStatus = () => {
|
||||
secondary={data.mac_address}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<ComputerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.AP_CLIENTS()} secondary={data.station_num} />
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default APStatus;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
@@ -17,6 +19,12 @@ import { useInterval } from 'utils';
|
||||
import { readActivity } from '../../api/app';
|
||||
import type { Stat } from '../main/types';
|
||||
|
||||
const QUALITY_COLORS = {
|
||||
PERFECT: '#00FF7F',
|
||||
WARNING: 'orange',
|
||||
POOR: 'red'
|
||||
} as const;
|
||||
|
||||
const SystemActivity = () => {
|
||||
const { data, send: loadData, error } = useRequest(readActivity);
|
||||
|
||||
@@ -28,14 +36,16 @@ const SystemActivity = () => {
|
||||
|
||||
useLayoutTitle(LL.DATA_TRAFFIC());
|
||||
|
||||
const stats_theme = tableTheme({
|
||||
Table: `
|
||||
const stats_theme = tableTheme(
|
||||
useMemo(
|
||||
() => ({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||
`,
|
||||
BaseRow: `
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
@@ -45,7 +55,7 @@ const SystemActivity = () => {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
Row: `
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
@@ -59,34 +69,40 @@ const SystemActivity = () => {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
BaseCell: `
|
||||
&:not(:first-of-type) {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
});
|
||||
}),
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
||||
const showName = (id: number) => {
|
||||
const name: keyof Translation['STATUS_NAMES'] =
|
||||
id.toString() as keyof Translation['STATUS_NAMES'];
|
||||
return LL.STATUS_NAMES[name]();
|
||||
};
|
||||
const showName = useCallback(
|
||||
(id: number) => {
|
||||
const name: keyof Translation['STATUS_NAMES'] =
|
||||
id.toString() as keyof Translation['STATUS_NAMES'];
|
||||
return LL.STATUS_NAMES[name]();
|
||||
},
|
||||
[LL]
|
||||
);
|
||||
|
||||
const showQuality = (stat: Stat) => {
|
||||
const showQuality = useCallback((stat: Stat) => {
|
||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||
return;
|
||||
}
|
||||
if (stat.q === 100) {
|
||||
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
|
||||
return <div style={{ color: QUALITY_COLORS.PERFECT }}>{stat.q}%</div>;
|
||||
}
|
||||
if (stat.q >= 95) {
|
||||
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
|
||||
return <div style={{ color: QUALITY_COLORS.WARNING }}>{stat.q}%</div>;
|
||||
} else {
|
||||
return <div style={{ color: 'red' }}>{stat.q}%</div>;
|
||||
return <div style={{ color: QUALITY_COLORS.POOR }}>{stat.q}%</div>;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const content = () => {
|
||||
const content = useMemo(() => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
||||
}
|
||||
@@ -121,9 +137,9 @@ const SystemActivity = () => {
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
}, [data, loadData, error?.message, stats_theme, LL, showName, showQuality]);
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
return <SectionContent>{content}</SectionContent>;
|
||||
};
|
||||
|
||||
export default SystemActivity;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
|
||||
import DevicesIcon from '@mui/icons-material/Devices';
|
||||
@@ -24,10 +26,61 @@ import { useInterval } from 'utils';
|
||||
|
||||
import BBQKeesIcon from './bbqkees.svg';
|
||||
|
||||
// Constants
|
||||
const AVATAR_COLORS = {
|
||||
DEFAULT: '#5f9a5f',
|
||||
BBQKEES: '#003289'
|
||||
} as const;
|
||||
|
||||
const TEMP_THRESHOLD_CELSIUS = 90; // Temperature threshold to determine F vs C
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
function formatTemperature(temp?: number): string {
|
||||
if (!temp) return '';
|
||||
const unit = temp > TEMP_THRESHOLD_CELSIUS ? 'F' : 'C';
|
||||
return `, T: ${temp} °${unit}`;
|
||||
}
|
||||
|
||||
function formatFlashSpeed(speed: number): string {
|
||||
return (speed / 1000000).toFixed(0) + ' MHz';
|
||||
}
|
||||
|
||||
function formatCPUCores(cores: number): string {
|
||||
return cores === 1 ? 'single-core)' : 'dual-core)';
|
||||
}
|
||||
|
||||
// Reusable component for hardware status list items
|
||||
interface HardwareListItemProps {
|
||||
icon: ReactElement;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
avatarColor?: string;
|
||||
customIcon?: ReactElement | undefined;
|
||||
}
|
||||
|
||||
const HardwareListItem = ({
|
||||
icon,
|
||||
primary,
|
||||
secondary,
|
||||
avatarColor = AVATAR_COLORS.DEFAULT,
|
||||
customIcon
|
||||
}: HardwareListItemProps) => (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: avatarColor, color: 'white' }}>
|
||||
{customIcon || icon}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={primary} secondary={secondary} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
);
|
||||
|
||||
const HardwareStatus = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -39,175 +92,72 @@ const HardwareStatus = () => {
|
||||
void loadData();
|
||||
});
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
{data.model ? (
|
||||
<Avatar sx={{ bgcolor: '#003289', color: 'white' }}>
|
||||
<img
|
||||
alt="BBQKees"
|
||||
src={BBQKeesIcon}
|
||||
style={{ width: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<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' +
|
||||
// bit of a hack : if the CPU temp is higher than 90 (=32 Fahrenheit if using Celsius), show F, otherwise C
|
||||
(data.temperature
|
||||
? ', T: ' +
|
||||
data.temperature +
|
||||
' °' +
|
||||
(data.temperature > 90 ? 'F' : 'C')
|
||||
: '')
|
||||
}
|
||||
/>
|
||||
</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, ' +
|
||||
formatNumber(data.free_caps) +
|
||||
' KB caps)'
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
<SectionContent>
|
||||
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
return (
|
||||
<SectionContent>
|
||||
<List>
|
||||
<HardwareListItem
|
||||
icon={<TapAndPlayIcon />}
|
||||
primary={`${LL.HARDWARE()} ${LL.DEVICE()}`}
|
||||
secondary={data.model || data.cpu_type}
|
||||
avatarColor={data.model ? AVATAR_COLORS.BBQKEES : AVATAR_COLORS.DEFAULT}
|
||||
customIcon={
|
||||
data.model ? (
|
||||
<img
|
||||
alt="BBQKees"
|
||||
src={BBQKeesIcon}
|
||||
style={{ width: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<HardwareListItem
|
||||
icon={<DevicesIcon />}
|
||||
primary="SDK"
|
||||
secondary={`${data.arduino_version} / ESP-IDF ${data.sdk_version}`}
|
||||
/>
|
||||
<HardwareListItem
|
||||
icon={<DeveloperBoardIcon />}
|
||||
primary="CPU"
|
||||
secondary={`${data.esp_platform}/${data.cpu_type} (rev.${data.cpu_rev}, ${formatCPUCores(data.cpu_cores)} @ ${data.cpu_freq_mhz} Mhz${formatTemperature(data.temperature)}`}
|
||||
/>
|
||||
<HardwareListItem
|
||||
icon={<MemoryIcon />}
|
||||
primary={LL.FREE_MEMORY()}
|
||||
secondary={`${formatNumber(data.free_heap)} KB (${formatNumber(data.max_alloc_heap)} KB max alloc, ${formatNumber(data.free_caps)} KB caps)`}
|
||||
/>
|
||||
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
||||
<HardwareListItem
|
||||
icon={<AppsIcon />}
|
||||
primary={LL.PSRAM()}
|
||||
secondary={`${formatNumber(data.psram_size)} KB / ${formatNumber(data.free_psram)} KB`}
|
||||
/>
|
||||
)}
|
||||
<HardwareListItem
|
||||
icon={<SdStorageIcon />}
|
||||
primary={LL.FLASH()}
|
||||
secondary={`${formatNumber(data.flash_chip_size)} KB , ${formatFlashSpeed(data.flash_chip_speed)}`}
|
||||
/>
|
||||
<HardwareListItem
|
||||
icon={<SdCardAlertIcon />}
|
||||
primary={LL.APPSIZE()}
|
||||
secondary={`${data.partition}: ${formatNumber(data.app_used)} KB / ${formatNumber(data.app_free)} KB`}
|
||||
/>
|
||||
<HardwareListItem
|
||||
icon={<FolderIcon />}
|
||||
primary={LL.FILESYSTEM()}
|
||||
secondary={`${formatNumber(data.fs_used)} KB / ${formatNumber(data.fs_free)} KB`}
|
||||
/>
|
||||
</List>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default HardwareStatus;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type FC, memo, useMemo } from 'react';
|
||||
|
||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import ReportIcon from '@mui/icons-material/Report';
|
||||
@@ -22,17 +24,28 @@ import type { MqttStatusType } from 'types';
|
||||
import { MqttDisconnectReason } from 'types';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
// Disconnect reason lookup table - created once, reused across renders
|
||||
const DISCONNECT_REASONS: Record<MqttDisconnectReason, string> = {
|
||||
[MqttDisconnectReason.USER_OK]: 'User disconnected',
|
||||
[MqttDisconnectReason.TCP_DISCONNECTED]: 'TCP disconnected',
|
||||
[MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION]:
|
||||
'Unacceptable protocol version',
|
||||
[MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED]: 'Client ID rejected',
|
||||
[MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE]: 'Server unavailable',
|
||||
[MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS]: 'Malformed credentials',
|
||||
[MqttDisconnectReason.MQTT_NOT_AUTHORIZED]: 'Not authorized',
|
||||
[MqttDisconnectReason.TLS_BAD_FINGERPRINT]: 'TLS fingerprint invalid'
|
||||
};
|
||||
|
||||
const getDisconnectReason = (disconnect_reason: MqttDisconnectReason): string =>
|
||||
DISCONNECT_REASONS[disconnect_reason] ?? 'Unknown';
|
||||
|
||||
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;
|
||||
if (!enabled) return theme.palette.info.main;
|
||||
return connected ? theme.palette.success.main : theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttPublishHighlight = (
|
||||
@@ -41,114 +54,100 @@ export const mqttPublishHighlight = (
|
||||
) => {
|
||||
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;
|
||||
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatusType, theme: Theme) =>
|
||||
mqtt_queued <= 1 ? theme.palette.success.main : theme.palette.warning.main;
|
||||
|
||||
return theme.palette.warning.main;
|
||||
};
|
||||
interface ConnectionStatusProps {
|
||||
data: MqttStatusType;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
// Memoized component to prevent unnecessary re-renders when parent updates
|
||||
const ConnectionStatus: FC<ConnectionStatusProps> = memo(({ data, theme }) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!data.connected && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ReportIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.DISCONNECT_REASON()}
|
||||
secondary={getDisconnectReason(data.disconnect_reason)}
|
||||
/>
|
||||
</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" />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const MqttStatus = () => {
|
||||
const { data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
|
||||
const { LL } = useI18nContext();
|
||||
const theme = useTheme();
|
||||
|
||||
useLayoutTitle('MQTT');
|
||||
|
||||
useInterval(() => {
|
||||
void loadData();
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle('MQTT');
|
||||
// Memoize error message separately to avoid re-renders on error object changes
|
||||
const errorMessage = error?.message || '';
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatusType) => {
|
||||
if (!enabled) {
|
||||
return LL.NOT_ENABLED();
|
||||
}
|
||||
if (connected) {
|
||||
return LL.CONNECTED(0) + ' (' + connect_count + ')';
|
||||
}
|
||||
return LL.DISCONNECTED() + ' (' + 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" />
|
||||
</>
|
||||
);
|
||||
const mqttStatusText = useMemo(() => {
|
||||
if (!data) return '';
|
||||
if (!data.enabled) return LL.NOT_ENABLED();
|
||||
return data.connected
|
||||
? `${LL.CONNECTED(0)} (${data.connect_count})`
|
||||
: `${LL.DISCONNECTED()} (${data.connect_count})`;
|
||||
}, [data, LL]);
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<SectionContent>
|
||||
<FormLoader onRetry={loadData} errorMessage={errorMessage} />
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
@@ -156,15 +155,13 @@ const MqttStatus = () => {
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatus(data)} />
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatusText} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{data.enabled && renderConnectionStatus()}
|
||||
{data.enabled && <ConnectionStatus data={data} theme={theme} />}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default MqttStatus;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
||||
@@ -23,6 +25,23 @@ import { NTPSyncStatus } from 'types';
|
||||
import { useInterval } from 'utils';
|
||||
import { formatDateTime } from 'utils';
|
||||
|
||||
// Utility functions
|
||||
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 NTPStatus = () => {
|
||||
const { data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
|
||||
|
||||
@@ -33,24 +52,6 @@ const NTPStatus = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle('NTP');
|
||||
|
||||
NTPApi.updateTime;
|
||||
|
||||
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 theme = useTheme();
|
||||
|
||||
const ntpStatus = ({ status }: NTPStatusType) => {
|
||||
@@ -66,66 +67,64 @@ const NTPStatus = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
const content = useMemo(() => {
|
||||
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>
|
||||
</>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
}, [data, error, loadData, LL, theme]);
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
return <SectionContent>{content}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NTPStatus;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import GiteIcon from '@mui/icons-material/Gite';
|
||||
@@ -25,10 +27,17 @@ import type { NetworkStatusType } from 'types';
|
||||
import { NetworkConnectionStatus } from 'types';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
// Utility functions
|
||||
const isConnected = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
export const isWiFi = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||
|
||||
export const isEthernet = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
const networkStatusHighlight = ({ status }: NetworkStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
@@ -55,11 +64,6 @@ const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
|
||||
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';
|
||||
@@ -81,6 +85,33 @@ const IPs = (status: NetworkStatusType) => {
|
||||
return status.local_ip + ', ' + status.local_ipv6;
|
||||
};
|
||||
|
||||
const getNetworkStatusText = (
|
||||
status: NetworkConnectionStatus,
|
||||
reconnectCount: number,
|
||||
LL: ReturnType<typeof useI18nContext>['LL']
|
||||
) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
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) (' + reconnectCount + ')';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0) + ' (' + reconnectCount + ')';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST() + ' (' + reconnectCount + ')';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const NetworkStatus = () => {
|
||||
const { data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
|
||||
|
||||
@@ -93,51 +124,30 @@ const NetworkStatus = () => {
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const networkStatus = ({ status }: NetworkStatusType) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
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.reconnect_count + ')';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return (
|
||||
LL.CONNECTED(1) + ' ' + LL.FAILED(0) + ' (' + data.reconnect_count + ')'
|
||||
);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST() + ' (' + data.reconnect_count + ')';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
const content = useMemo(() => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
||||
}
|
||||
|
||||
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
|
||||
const statusColor = networkStatusHighlight(data, theme);
|
||||
const qualityColor = networkQualityHighlight(data, theme);
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
||||
<Avatar sx={{ bgcolor: statusColor }}>
|
||||
{isWiFi(data) && <WifiIcon />}
|
||||
{isEthernet(data) && <RouterIcon />}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||
<ListItemText primary="Status" secondary={statusText} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
||||
<Avatar sx={{ bgcolor: statusColor }}>
|
||||
<GiteIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
@@ -148,13 +158,13 @@ const NetworkStatus = () => {
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkQualityHighlight(data, theme) }}>
|
||||
<Avatar sx={{ bgcolor: qualityColor }}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="SSID (RSSI)"
|
||||
secondary={data.ssid + ' (' + data.rssi + ' dBm)'}
|
||||
secondary={`${data.ssid} (${data.rssi} dBm)`}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
@@ -218,9 +228,9 @@ const NetworkStatus = () => {
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
}, [data, error, loadData, LL, theme]);
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
return <SectionContent>{content}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NetworkStatus;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
@@ -43,6 +43,28 @@ import { formatDateTime } from 'utils/time';
|
||||
|
||||
import SystemMonitor from './SystemMonitor';
|
||||
|
||||
// Pure functions moved outside component to avoid recreation on each render
|
||||
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
|
||||
|
||||
const formatDurationSec = (
|
||||
duration_sec: number,
|
||||
LL: ReturnType<typeof useI18nContext>['LL']
|
||||
) => {
|
||||
const ms = duration_sec * 1000;
|
||||
const days = Math.trunc(ms / 86400000);
|
||||
const hours = Math.trunc(ms / 3600000) % 24;
|
||||
const minutes = Math.trunc(ms / 60000) % 60;
|
||||
const seconds = Math.trunc(ms / 1000) % 60;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (days) parts.push(LL.NUM_DAYS({ num: days }));
|
||||
if (hours) parts.push(LL.NUM_HOURS({ num: hours }));
|
||||
if (minutes) parts.push(LL.NUM_MINUTES({ num: minutes }));
|
||||
parts.push(LL.NUM_SECONDS({ num: seconds }));
|
||||
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
const SystemStatus = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -76,51 +98,25 @@ const SystemStatus = () => {
|
||||
|
||||
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;
|
||||
// Memoize derived status values to avoid recalculation on every render
|
||||
const busStatus = useMemo(() => {
|
||||
if (!data) return 'EMS state unknown';
|
||||
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += LL.NUM_DAYS({ num: days }) + ' ';
|
||||
switch (data.bus_status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return 'EMS ' + LL.TX_ISSUES();
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return 'EMS ' + LL.DISCONNECTED();
|
||||
default:
|
||||
return 'EMS state unknown';
|
||||
}
|
||||
if (hours) {
|
||||
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
|
||||
}
|
||||
formatted += LL.NUM_SECONDS({ num: seconds });
|
||||
return formatted;
|
||||
};
|
||||
}, [data?.bus_status, data?.bus_uptime, LL]);
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
const busStatusHighlight = useMemo(() => {
|
||||
if (!data) return theme.palette.warning.main;
|
||||
|
||||
const busStatus = () => {
|
||||
if (data) {
|
||||
switch (data.bus_status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return (
|
||||
'EMS ' +
|
||||
LL.CONNECTED(0) +
|
||||
' (' +
|
||||
formatDurationSec(data.bus_uptime) +
|
||||
')'
|
||||
);
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return 'EMS ' + LL.TX_ISSUES();
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return 'EMS ' + LL.DISCONNECTED();
|
||||
}
|
||||
}
|
||||
return 'EMS state unknown';
|
||||
};
|
||||
|
||||
const busStatusHighlight = () => {
|
||||
switch (data.bus_status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
@@ -131,27 +127,28 @@ const SystemStatus = () => {
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
}, [data?.bus_status, theme.palette]);
|
||||
|
||||
const ntpStatus = useMemo(() => {
|
||||
if (!data) return LL.UNKNOWN();
|
||||
|
||||
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() +
|
||||
(data.ntp_time !== undefined
|
||||
? ' (' + formatDateTime(data.ntp_time) + ')'
|
||||
: '')
|
||||
);
|
||||
return data.ntp_time
|
||||
? `${LL.ACTIVE()} (${formatDateTime(data.ntp_time)})`
|
||||
: LL.ACTIVE();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
}, [data?.ntp_status, data?.ntp_time, LL]);
|
||||
|
||||
const ntpStatusHighlight = useMemo(() => {
|
||||
if (!data) return theme.palette.error.main;
|
||||
|
||||
const ntpStatusHighlight = () => {
|
||||
switch (data.ntp_status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return theme.palette.info.main;
|
||||
@@ -162,9 +159,11 @@ const SystemStatus = () => {
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
}, [data?.ntp_status, theme.palette]);
|
||||
|
||||
const networkStatusHighlight = useMemo(() => {
|
||||
if (!data) return theme.palette.warning.main;
|
||||
|
||||
const networkStatusHighlight = () => {
|
||||
switch (data.network_status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
@@ -179,9 +178,11 @@ const SystemStatus = () => {
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
}, [data?.network_status, theme.palette]);
|
||||
|
||||
const networkStatus = useMemo(() => {
|
||||
if (!data) return LL.UNKNOWN();
|
||||
|
||||
const networkStatus = () => {
|
||||
switch (data.network_status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
@@ -190,24 +191,27 @@ const SystemStatus = () => {
|
||||
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)';
|
||||
return `${LL.CONNECTED(0)} (WiFi, ${data.wifi_rssi} dBm)`;
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
return `${LL.CONNECTED(0)} (Ethernet)`;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
|
||||
return `${LL.CONNECTED(1)} ${LL.FAILED(0)}`;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||
return `${LL.CONNECTED(1)} ${LL.LOST()}`;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
}, [data?.network_status, data?.wifi_rssi, LL]);
|
||||
|
||||
const activeHighlight = (value: boolean) =>
|
||||
value ? theme.palette.success.main : theme.palette.info.main;
|
||||
const activeHighlight = useCallback(
|
||||
(value: boolean) =>
|
||||
value ? theme.palette.success.main : theme.palette.info.main,
|
||||
[theme.palette]
|
||||
);
|
||||
|
||||
const doRestart = async () => {
|
||||
const doRestart = useCallback(async () => {
|
||||
setConfirmRestart(false);
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
@@ -215,38 +219,83 @@ const SystemStatus = () => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
}, [sendAPI]);
|
||||
|
||||
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)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={doRestart}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
const handleCloseRestartDialog = useCallback(() => {
|
||||
setConfirmRestart(false);
|
||||
}, []);
|
||||
|
||||
const renderRestartDialog = useMemo(
|
||||
() => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmRestart}
|
||||
onClose={handleCloseRestartDialog}
|
||||
>
|
||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleCloseRestartDialog}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={doRestart}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
),
|
||||
[confirmRestart, handleCloseRestartDialog, doRestart, LL]
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
// Memoize formatted values
|
||||
const firmwareVersion = useMemo(
|
||||
() => `v${data?.emsesp_version || ''}`,
|
||||
[data?.emsesp_version]
|
||||
);
|
||||
|
||||
const uptimeText = useMemo(
|
||||
() => (data ? formatDurationSec(data.uptime, LL) : ''),
|
||||
[data?.uptime, LL]
|
||||
);
|
||||
|
||||
const freeMemoryText = useMemo(
|
||||
() => (data ? `${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}` : ''),
|
||||
[data?.free_heap, LL]
|
||||
);
|
||||
|
||||
const networkIcon = useMemo(
|
||||
() =>
|
||||
data?.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
||||
? WifiIcon
|
||||
: RouterIcon,
|
||||
[data?.network_status]
|
||||
);
|
||||
|
||||
const mqttStatusText = useMemo(
|
||||
() => (data?.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)),
|
||||
[data?.mqtt_status, LL]
|
||||
);
|
||||
|
||||
const apStatusText = useMemo(
|
||||
() => (data?.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)),
|
||||
[data?.ap_status, LL]
|
||||
);
|
||||
|
||||
const handleRestartClick = useCallback(() => {
|
||||
setConfirmRestart(true);
|
||||
}, []);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!data || !LL) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
||||
}
|
||||
@@ -258,7 +307,7 @@ const SystemStatus = () => {
|
||||
icon={BuildIcon}
|
||||
bgcolor="#72caf9"
|
||||
label="EMS-ESP Firmware"
|
||||
text={'v' + data.emsesp_version}
|
||||
text={firmwareVersion}
|
||||
to="version"
|
||||
/>
|
||||
|
||||
@@ -268,16 +317,13 @@ const SystemStatus = () => {
|
||||
<TimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.UPTIME()}
|
||||
secondary={formatDurationSec(data.uptime)}
|
||||
/>
|
||||
<ListItemText primary={LL.UPTIME()} secondary={uptimeText} />
|
||||
{me.admin && (
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setConfirmRestart(true)}
|
||||
onClick={handleRestartClick}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
@@ -289,29 +335,25 @@ const SystemStatus = () => {
|
||||
icon={MemoryIcon}
|
||||
bgcolor="#68374d"
|
||||
label={LL.HARDWARE()}
|
||||
text={formatNumber(data.free_heap) + ' KB' + ' ' + LL.FREE_MEMORY()}
|
||||
text={freeMemoryText}
|
||||
to="/status/hardwarestatus"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={DirectionsBusIcon}
|
||||
bgcolor={busStatusHighlight()}
|
||||
bgcolor={busStatusHighlight}
|
||||
label={LL.DATA_TRAFFIC()}
|
||||
text={busStatus()}
|
||||
text={busStatus}
|
||||
to="/status/activity"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={
|
||||
data.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
||||
? WifiIcon
|
||||
: RouterIcon
|
||||
}
|
||||
bgcolor={networkStatusHighlight()}
|
||||
icon={networkIcon}
|
||||
bgcolor={networkStatusHighlight}
|
||||
label={LL.NETWORK(1)}
|
||||
text={networkStatus()}
|
||||
text={networkStatus}
|
||||
to="/status/network"
|
||||
/>
|
||||
|
||||
@@ -320,16 +362,16 @@ const SystemStatus = () => {
|
||||
icon={DeviceHubIcon}
|
||||
bgcolor={activeHighlight(data.mqtt_status)}
|
||||
label="MQTT"
|
||||
text={data.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)}
|
||||
text={mqttStatusText}
|
||||
to="/status/mqtt"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor={ntpStatusHighlight()}
|
||||
bgcolor={ntpStatusHighlight}
|
||||
label="NTP"
|
||||
text={ntpStatus()}
|
||||
text={ntpStatus}
|
||||
to="/status/ntp"
|
||||
/>
|
||||
|
||||
@@ -338,7 +380,7 @@ const SystemStatus = () => {
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor={activeHighlight(data.ap_status)}
|
||||
label={LL.ACCESS_POINT(0)}
|
||||
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||
text={apStatusText}
|
||||
to="/status/ap"
|
||||
/>
|
||||
|
||||
@@ -352,14 +394,33 @@ const SystemStatus = () => {
|
||||
/>
|
||||
</List>
|
||||
|
||||
{renderRestartDialog()}
|
||||
{renderRestartDialog}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}, [
|
||||
data,
|
||||
LL,
|
||||
firmwareVersion,
|
||||
uptimeText,
|
||||
freeMemoryText,
|
||||
networkIcon,
|
||||
mqttStatusText,
|
||||
apStatusText,
|
||||
busStatus,
|
||||
busStatusHighlight,
|
||||
networkStatusHighlight,
|
||||
networkStatus,
|
||||
ntpStatusHighlight,
|
||||
ntpStatus,
|
||||
activeHighlight,
|
||||
me.admin,
|
||||
handleRestartClick,
|
||||
error,
|
||||
loadData,
|
||||
renderRestartDialog
|
||||
]);
|
||||
|
||||
return (
|
||||
<SectionContent>{restarting ? <SystemMonitor /> : content()}</SectionContent>
|
||||
);
|
||||
return <SectionContent>{restarting ? <SystemMonitor /> : content}</SectionContent>;
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
@@ -31,6 +31,8 @@ import type { LogEntry, LogSettings } from 'types';
|
||||
import { LogLevel } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
|
||||
const MAX_LOG_ENTRIES = 1000; // Limit log entries to prevent memory issues
|
||||
|
||||
const TextColors: Record<LogLevel, string> = {
|
||||
[LogLevel.ERROR]: '#ff0000', // red
|
||||
[LogLevel.WARNING]: '#ff0000', // red
|
||||
@@ -47,11 +49,6 @@ const LogEntryLine = styled('span')(
|
||||
})
|
||||
);
|
||||
|
||||
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:
|
||||
@@ -71,6 +68,36 @@ const levelLabel = (level: LogLevel) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Memoized log entry component to prevent unnecessary re-renders
|
||||
const LogEntryItem = memo(
|
||||
({ entry, compact }: { entry: LogEntry; compact: boolean }) => {
|
||||
const paddedLevelLabel = (level: LogLevel) => {
|
||||
const label = levelLabel(level);
|
||||
return compact ? ' ' + label[0] : label.padStart(8, '\xa0');
|
||||
};
|
||||
|
||||
const paddedNameLabel = (name: string) => {
|
||||
const label = '[' + name + ']';
|
||||
return compact ? label : label.padEnd(12, '\xa0');
|
||||
};
|
||||
|
||||
const paddedIDLabel = (id: number) => {
|
||||
const label = id + ':';
|
||||
return compact ? label : label.padEnd(7, '\xa0');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ font: '14px monospace', whiteSpace: 'nowrap' }}>
|
||||
<span>{entry.t}</span>
|
||||
<span>{paddedLevelLabel(entry.l)} </span>
|
||||
<span>{paddedIDLabel(entry.i)} </span>
|
||||
<span>{paddedNameLabel(entry.n)} </span>
|
||||
<LogEntryLine details={{ level: entry.l }}>{entry.m}</LogEntryLine>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SystemLog = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -107,7 +134,7 @@ const SystemLog = () => {
|
||||
const ALPHA_NUMERIC_DASH_REGEX = /^[a-fA-F0-9 ]+$/;
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
@@ -121,7 +148,13 @@ const SystemLog = () => {
|
||||
const rawData = message.data;
|
||||
const logentry = JSON.parse(rawData) as LogEntry;
|
||||
if (lastId < logentry.i) {
|
||||
setLogEntries((log) => [...log, logentry]);
|
||||
setLogEntries((log) => {
|
||||
const newLog = [...log, logentry];
|
||||
// Limit log entries to prevent memory issues
|
||||
return newLog.length > MAX_LOG_ENTRIES
|
||||
? newLog.slice(-MAX_LOG_ENTRIES)
|
||||
: newLog;
|
||||
});
|
||||
setLastId(logentry.i);
|
||||
}
|
||||
})
|
||||
@@ -129,27 +162,11 @@ const SystemLog = () => {
|
||||
toast.error('No connection to Log service');
|
||||
});
|
||||
|
||||
const paddedLevelLabel = (level: LogLevel) => {
|
||||
const label = levelLabel(level);
|
||||
return data?.compact ? ' ' + label[0] : label.padStart(8, '\xa0');
|
||||
};
|
||||
const onDownload = useCallback(() => {
|
||||
const result = logEntries
|
||||
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
|
||||
.join('\n');
|
||||
|
||||
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',
|
||||
@@ -159,11 +176,11 @@ const SystemLog = () => {
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
}, [logEntries]);
|
||||
|
||||
const saveSettings = async () => {
|
||||
const saveSettings = useCallback(async () => {
|
||||
await saveData();
|
||||
};
|
||||
}, [saveData]);
|
||||
|
||||
// handle scrolling
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -174,9 +191,9 @@ const SystemLog = () => {
|
||||
block: 'end'
|
||||
});
|
||||
}
|
||||
}, [logEntries.length]);
|
||||
}, [logEntries.length, autoscroll]);
|
||||
|
||||
const sendReadCommand = () => {
|
||||
const sendReadCommand = useCallback(() => {
|
||||
if (readValue === '') {
|
||||
setReadOpen(!readOpen);
|
||||
return;
|
||||
@@ -187,7 +204,17 @@ const SystemLog = () => {
|
||||
setReadOpen(false);
|
||||
setReadValue('');
|
||||
}
|
||||
};
|
||||
}, [readValue, readOpen, send]);
|
||||
|
||||
// Memoize box positioning to avoid recalculating on every render
|
||||
const boxPosition = useMemo(() => {
|
||||
const logWindow = document.getElementById('log-window');
|
||||
if (!logWindow) {
|
||||
return { top: 0, left: 0 };
|
||||
}
|
||||
const rect = logWindow.getBoundingClientRect();
|
||||
return { top: rect.bottom, left: rect.left };
|
||||
}, [data]); // Recalculate only when data changes (settings may affect layout)
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
@@ -332,21 +359,13 @@ const SystemLog = () => {
|
||||
position: 'absolute',
|
||||
right: 18,
|
||||
bottom: 18,
|
||||
left: () => leftOffset(),
|
||||
top: () => topOffset(),
|
||||
left: boxPosition.left,
|
||||
top: boxPosition.top - 110,
|
||||
p: 1
|
||||
}}
|
||||
>
|
||||
{logEntries.map((e) => (
|
||||
<div key={e.i} style={{ font: '14px monospace', whiteSpace: 'nowrap' }}>
|
||||
<span>{e.t}</span>
|
||||
<span>{paddedLevelLabel(e.l)} </span>
|
||||
<span>{paddedIDLabel(e.i)} </span>
|
||||
<span>{paddedNameLabel(e.n)} </span>
|
||||
<LogEntryLine details={{ level: e.l }} key={e.i}>
|
||||
{e.m}
|
||||
</LogEntryLine>
|
||||
</div>
|
||||
<LogEntryItem key={e.i} entry={e} compact={data.compact} />
|
||||
))}
|
||||
|
||||
<div ref={ref} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import { Box, Button, Dialog, DialogContent, Typography } from '@mui/material';
|
||||
@@ -17,11 +17,9 @@ import { LinearProgressWithLabel } from '../../components/upload/LinearProgressW
|
||||
|
||||
const SystemMonitor = () => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const hasInitialized = useRef(false);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
let count = 0;
|
||||
|
||||
const { send: setSystemStatus } = useRequest(
|
||||
(status: string) => callAction({ action: 'systemStatus', param: status }),
|
||||
{
|
||||
@@ -32,10 +30,12 @@ const SystemMonitor = () => {
|
||||
const { data, send } = useRequest(readSystemStatus, {
|
||||
force: true,
|
||||
async middleware(_, next) {
|
||||
if (count++ >= 1) {
|
||||
// skip first request (1 second) to allow AsyncWS to send its response
|
||||
await next();
|
||||
// Skip first request to allow AsyncWS to send its response
|
||||
if (!hasInitialized.current) {
|
||||
hasInitialized.current = true;
|
||||
return; // Don't await next() on first call
|
||||
}
|
||||
await next();
|
||||
}
|
||||
})
|
||||
.onSuccess((event) => {
|
||||
@@ -58,13 +58,41 @@ const SystemMonitor = () => {
|
||||
void send();
|
||||
}, 1000); // check every 1 second
|
||||
|
||||
const onCancel = async () => {
|
||||
const { statusMessage, isUploading, progressValue } = useMemo(() => {
|
||||
const status = data?.status;
|
||||
|
||||
let message = '';
|
||||
if (status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING) {
|
||||
message = LL.WAIT_FIRMWARE();
|
||||
} else if (status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART) {
|
||||
message = LL.APPLICATION_RESTARTING();
|
||||
} else if (status === SystemStatusCodes.SYSTEM_STATUS_NORMAL) {
|
||||
message = LL.RESTARTING_PRE();
|
||||
} else if (status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD) {
|
||||
message = 'Upload Failed';
|
||||
} else {
|
||||
message = LL.RESTARTING_POST();
|
||||
}
|
||||
|
||||
const uploading =
|
||||
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
|
||||
const progress =
|
||||
uploading && status
|
||||
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
statusMessage: message,
|
||||
isUploading: uploading,
|
||||
progressValue: progress
|
||||
};
|
||||
}, [data?.status, LL]);
|
||||
|
||||
const onCancel = useCallback(async () => {
|
||||
setErrorMessage(undefined);
|
||||
await setSystemStatus(
|
||||
SystemStatusCodes.SYSTEM_STATUS_NORMAL as unknown as string
|
||||
);
|
||||
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
||||
document.location.href = '/';
|
||||
};
|
||||
}, [setSystemStatus]);
|
||||
|
||||
return (
|
||||
<Dialog fullWidth={true} sx={dialogStyle} open={true}>
|
||||
@@ -76,15 +104,7 @@ const SystemMonitor = () => {
|
||||
fontWeight={400}
|
||||
textAlign="center"
|
||||
>
|
||||
{data?.status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
||||
? LL.WAIT_FIRMWARE()
|
||||
: data?.status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
|
||||
? LL.APPLICATION_RESTARTING()
|
||||
: data?.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
|
||||
? LL.RESTARTING_PRE()
|
||||
: data?.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
||||
? 'Upload Failed'
|
||||
: LL.RESTARTING_POST()}
|
||||
{statusMessage}
|
||||
</Typography>
|
||||
|
||||
{errorMessage ? (
|
||||
@@ -105,13 +125,9 @@ const SystemMonitor = () => {
|
||||
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
|
||||
{LL.PLEASE_WAIT()}…
|
||||
</Typography>
|
||||
{data && data.status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING && (
|
||||
{isUploading && (
|
||||
<Box width="100%" pl={2} pr={2} py={2}>
|
||||
<LinearProgressWithLabel
|
||||
value={Math.round(
|
||||
data?.status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
||||
)}
|
||||
/>
|
||||
<LinearProgressWithLabel value={progressValue} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user