mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 00:39:50 +03:00
Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev2
This commit is contained in:
250
interface/src/framework/Settings.tsx
Normal file
250
interface/src/framework/Settings.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CastIcon from '@mui/icons-material/Cast';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
import { List, Button, Dialog, DialogActions, DialogContent, DialogTitle, Box } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, type FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from './system/RestartMonitor';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Settings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS(0));
|
||||
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const restart = async () => {
|
||||
setProcessing(true);
|
||||
await restartCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const factoryReset = async () => {
|
||||
setProcessing(true);
|
||||
await factoryResetCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmFactoryReset(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const partition = async () => {
|
||||
setProcessing(true);
|
||||
await partitionCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.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={restart}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={partition}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
>
|
||||
EMS-ESP Loader
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const renderFactoryResetDialog = () => (
|
||||
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(false)}
|
||||
disabled={processing}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={factoryReset}
|
||||
disabled={processing}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
{/* TODO: translate */}
|
||||
<ListMenuItem
|
||||
icon={TuneIcon}
|
||||
bgcolor="#134ba2"
|
||||
label={LL.APPLICATION_SETTINGS()}
|
||||
text="Modify EMS-ESP Application Settings"
|
||||
to="ems-esp"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsEthernetIcon}
|
||||
bgcolor="#40828f"
|
||||
label={LL.NETWORK(0)}
|
||||
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(0)))}
|
||||
to="network"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor="#5f9a5f"
|
||||
label={LL.ACCESS_POINT(0)}
|
||||
text={LL.CONFIGURE(LL.ACCESS_POINT(0))}
|
||||
to="ap"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor="#c5572c"
|
||||
label="NTP"
|
||||
text={LL.CONFIGURE(LL.LOCAL_TIME())}
|
||||
to="ntp"
|
||||
/>
|
||||
|
||||
<ListMenuItem icon={DeviceHubIcon} bgcolor="#68374d" label="MQTT" text={LL.CONFIGURE('MQTT')} to="mqtt" />
|
||||
|
||||
<ListMenuItem icon={CastIcon} bgcolor="#efc34b" label="OTA" text={LL.CONFIGURE('OTA')} to="ota" />
|
||||
|
||||
{/* TODO: translate */}
|
||||
<ListMenuItem icon={LockIcon} label={LL.SECURITY(0)} text="Add/Remove Users" to="security" />
|
||||
|
||||
<ListMenuItem
|
||||
icon={MemoryIcon}
|
||||
bgcolor="#b1395f"
|
||||
label={LL.STATUS_OF('ESP32')}
|
||||
text="ESP32 Information"
|
||||
to="espsystemstatus"
|
||||
/>
|
||||
|
||||
{/* TODO: translate */}
|
||||
<ListMenuItem
|
||||
icon={ImportExportIcon}
|
||||
bgcolor="#5d89f7"
|
||||
label={LL.UPLOAD_DOWNLOAD()}
|
||||
text="Upload/Download Settings and Firmware"
|
||||
to="upload"
|
||||
/>
|
||||
</List>
|
||||
|
||||
{renderRestartDialog()}
|
||||
{renderFactoryResetDialog()}
|
||||
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => setConfirmRestart(true)}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(true)}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -6,7 +6,7 @@ import { useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { APSettings } from 'types';
|
||||
import type { APSettingsType } from 'types';
|
||||
import * as APApi from 'api/ap';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
@@ -24,10 +24,10 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
|
||||
import { createAPSettingsValidator, validate } from 'validators';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettings) =>
|
||||
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
|
||||
const APSettingsForm: FC = () => {
|
||||
const APSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
@@ -39,7 +39,7 @@ const APSettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<APSettings>({
|
||||
} = useRest<APSettingsType>({
|
||||
read: APApi.readAPSettings,
|
||||
update: APApi.updateAPSettings
|
||||
});
|
||||
@@ -205,11 +205,11 @@ const APSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default APSettingsForm;
|
||||
export default APSettings;
|
||||
@@ -7,14 +7,14 @@ import { useRequest } from 'alova';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { APStatus } from 'types';
|
||||
import type { APStatusType } from 'types';
|
||||
import * as APApi from 'api/ap';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { APNetworkStatus } from 'types';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
@@ -27,14 +27,14 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
const APStatusForm: FC = () => {
|
||||
const APStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const apStatus = ({ status }: APStatus) => {
|
||||
const apStatus = ({ status }: APStatusType) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
@@ -99,11 +99,7 @@ const APStatusForm: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default APStatusForm;
|
||||
export default APStatus;
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { useContext } from 'react';
|
||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import APSettingsForm from './APSettingsForm';
|
||||
import APStatusForm from './APStatusForm';
|
||||
import APSettings from './APSettings';
|
||||
import APStatus from './APStatus';
|
||||
import type { FC } from 'react';
|
||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
@@ -15,32 +13,18 @@ const AccessPoint: FC = () => {
|
||||
|
||||
useLayoutTitle(LL.ACCESS_POINT(0));
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/ap/status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
|
||||
<Tab
|
||||
value="/ap/settings"
|
||||
label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} />
|
||||
<Tab value="status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<APStatusForm />} />
|
||||
<Route index element={<Navigate to="status" />} />
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<APSettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate replace to="/ap/status" />} />
|
||||
<Route path="status" element={<APStatus />} />
|
||||
<Route path="settings" element={<APSettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { useContext } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import MqttSettingsForm from './MqttSettingsForm';
|
||||
import MqttStatusForm from './MqttStatusForm';
|
||||
import MqttSettings from './MqttSettings';
|
||||
import MqttStatus from './MqttStatus';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
@@ -15,26 +13,18 @@ const Mqtt: FC = () => {
|
||||
|
||||
useLayoutTitle('MQTT');
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/mqtt/status" label={LL.STATUS_OF('MQTT')} />
|
||||
<Tab value="/mqtt/settings" label={LL.SETTINGS_OF('MQTT')} disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF('MQTT')} />
|
||||
<Tab value="status" label={LL.STATUS_OF('MQTT')} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<MqttStatusForm />} />
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<MqttSettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate replace to="/mqtt/status" />} />
|
||||
<Route path="status" element={<MqttStatus />} />
|
||||
<Route path="settings" element={<MqttSettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { MqttSettings } from 'types';
|
||||
import type { MqttSettingsType } from 'types';
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
@@ -21,7 +21,7 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
|
||||
import { createMqttSettingsValidator, validate } from 'validators';
|
||||
|
||||
const MqttSettingsForm: FC = () => {
|
||||
const MqttSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
@@ -33,7 +33,7 @@ const MqttSettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<MqttSettings>({
|
||||
} = useRest<MqttSettingsType>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
});
|
||||
@@ -464,11 +464,11 @@ const MqttSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default MqttSettingsForm;
|
||||
export default MqttSettings;
|
||||
@@ -8,13 +8,13 @@ import { useRequest } from 'alova';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { MqttStatus } from 'types';
|
||||
import type { MqttStatusType } from 'types';
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { MqttDisconnectReason } from 'types';
|
||||
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, theme: Theme) => {
|
||||
if (!enabled) {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
@@ -24,27 +24,27 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
||||
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 }: MqttStatus, theme: Theme) => {
|
||||
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatusType, theme: Theme) => {
|
||||
if (mqtt_queued <= 1) return theme.palette.success.main;
|
||||
|
||||
return theme.palette.warning.main;
|
||||
};
|
||||
|
||||
const MqttStatusForm: FC = () => {
|
||||
const MqttStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatus) => {
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatusType) => {
|
||||
if (!enabled) {
|
||||
return LL.NOT_ENABLED();
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const MqttStatusForm: FC = () => {
|
||||
return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
|
||||
};
|
||||
|
||||
const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||
const disconnectReason = ({ disconnect_reason }: MqttStatusType) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return 'TCP disconnected';
|
||||
@@ -146,11 +146,7 @@ const MqttStatusForm: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF('MQTT')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default MqttStatusForm;
|
||||
export default MqttStatus;
|
||||
59
interface/src/framework/network/Network.tsx
Normal file
59
interface/src/framework/network/Network.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import NetworkSettings from './NetworkSettings';
|
||||
import NetworkStatus from './NetworkStatus';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { WiFiNetwork } from 'types';
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Network: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.NETWORK(0));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||
|
||||
const selectNetwork = useCallback(
|
||||
(network: WiFiNetwork) => {
|
||||
setSelectedNetwork(network);
|
||||
navigate('settings');
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const deselectNetwork = useCallback(() => {
|
||||
setSelectedNetwork(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WiFiConnectionContext.Provider
|
||||
value={{
|
||||
selectedNetwork,
|
||||
selectNetwork,
|
||||
deselectNetwork
|
||||
}}
|
||||
>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} />
|
||||
<Tab value="status" label={LL.STATUS_OF(LL.NETWORK(1))} />
|
||||
<Tab value="scan" label={LL.NETWORK_SCAN()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<NetworkStatus />} />
|
||||
<Route path="scan" element={<WiFiNetworkScanner />} />
|
||||
<Route path="settings" element={<NetworkSettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</WiFiConnectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Network;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||
import NetworkStatusForm from './NetworkStatusForm';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { WiFiNetwork } from 'types';
|
||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const NetworkConnection: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.NETWORK(0));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||
|
||||
const selectNetwork = useCallback(
|
||||
(network: WiFiNetwork) => {
|
||||
setSelectedNetwork(network);
|
||||
navigate('settings');
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const deselectNetwork = useCallback(() => {
|
||||
setSelectedNetwork(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WiFiConnectionContext.Provider
|
||||
value={{
|
||||
selectedNetwork,
|
||||
selectNetwork,
|
||||
deselectNetwork
|
||||
}}
|
||||
>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/network/status" label={LL.STATUS_OF(LL.NETWORK(1))} />
|
||||
<Tab value="/network/scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/network/settings"
|
||||
label={LL.SETTINGS_OF(LL.NETWORK(1))}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<NetworkStatusForm />} />
|
||||
<Route
|
||||
path="scan"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<WiFiNetworkScanner />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<NetworkSettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate replace to="/network/status" />} />
|
||||
</Routes>
|
||||
</WiFiConnectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkConnection;
|
||||
@@ -28,7 +28,7 @@ import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NetworkSettings } from 'types';
|
||||
import type { NetworkSettingsType } from 'types';
|
||||
import * as NetworkApi from 'api/network';
|
||||
import * as SystemApi from 'api/system';
|
||||
import {
|
||||
@@ -48,7 +48,7 @@ import { updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { createNetworkSettingsValidator } from 'validators/network';
|
||||
|
||||
const WiFiSettingsForm: FC = () => {
|
||||
const NetworkSettings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||
@@ -68,7 +68,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
saveData,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
} = useRest<NetworkSettings>({
|
||||
} = useRest<NetworkSettingsType>({
|
||||
read: NetworkApi.readNetworkSettings,
|
||||
update: NetworkApi.updateNetworkSettings
|
||||
});
|
||||
@@ -135,7 +135,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
<Typography variant="h6" color="primary">
|
||||
WiFi
|
||||
</Typography>
|
||||
{selectedNetwork ? (
|
||||
@@ -360,11 +360,11 @@ const WiFiSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default WiFiSettingsForm;
|
||||
export default NetworkSettings;
|
||||
@@ -11,18 +11,18 @@ import { useRequest } from 'alova';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NetworkStatus } from 'types';
|
||||
import type { NetworkStatusType } from 'types';
|
||||
import * as NetworkApi from 'api/network';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { NetworkConnectionStatus } from 'types';
|
||||
|
||||
const isConnected = ({ status }: NetworkStatus) =>
|
||||
const isConnected = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
|
||||
const networkStatusHighlight = ({ status }: NetworkStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
@@ -39,7 +39,7 @@ const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
const networkQualityHighlight = ({ rssi }: NetworkStatus, theme: Theme) => {
|
||||
const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
|
||||
if (rssi <= -85) {
|
||||
return theme.palette.error.main;
|
||||
} else if (rssi <= -75) {
|
||||
@@ -48,17 +48,18 @@ const networkQualityHighlight = ({ rssi }: NetworkStatus, theme: Theme) => {
|
||||
return theme.palette.success.main;
|
||||
};
|
||||
|
||||
export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||
export const isEthernet = ({ status }: NetworkStatus) => 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 dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatus) => {
|
||||
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: NetworkStatus) => {
|
||||
const IPs = (status: NetworkStatusType) => {
|
||||
if (!status.local_ipv6 || status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000') {
|
||||
return status.local_ip;
|
||||
}
|
||||
@@ -68,14 +69,14 @@ const IPs = (status: NetworkStatus) => {
|
||||
return status.local_ip + ', ' + status.local_ipv6;
|
||||
};
|
||||
|
||||
const NetworkStatusForm: FC = () => {
|
||||
const NetworkStatus: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const networkStatus = ({ status }: NetworkStatus) => {
|
||||
const networkStatus = ({ status }: NetworkStatusType) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
@@ -193,11 +194,7 @@ const NetworkStatusForm: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF(LL.NETWORK(1))} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NetworkStatusForm;
|
||||
export default NetworkStatus;
|
||||
@@ -56,7 +56,7 @@ const WiFiNetworkScanner: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.NETWORK_SCANNER()}>
|
||||
<SectionContent>
|
||||
{renderNetworkScanner()}
|
||||
<ButtonRow>
|
||||
<Button
|
||||
|
||||
@@ -8,7 +8,7 @@ import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NTPSettings } from 'types';
|
||||
import type { NTPSettingsType } from 'types';
|
||||
import * as NTPApi from 'api/ntp';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
@@ -23,7 +23,7 @@ import { updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||
|
||||
const NTPSettingsForm: FC = () => {
|
||||
const NTPSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
@@ -35,7 +35,7 @@ const NTPSettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<NTPSettings>({
|
||||
} = useRest<NTPSettingsType>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
});
|
||||
@@ -130,11 +130,11 @@ const NTPSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default NTPSettingsForm;
|
||||
export default NTPSettings;
|
||||
@@ -22,44 +22,26 @@ import {
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NTPStatus } from 'types';
|
||||
import type { NTPStatusType } from 'types';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as NTPApi from 'api/ntp';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { NTPSyncStatus } from 'types';
|
||||
import { formatDateTime, formatLocalDateTime } from 'utils';
|
||||
|
||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
||||
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
|
||||
|
||||
export const ntpStatusHighlight = ({ status }: NTPStatus, 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 NTPStatusForm: FC = () => {
|
||||
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 { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -69,6 +51,22 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
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 = () => {
|
||||
@@ -78,7 +76,7 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const ntpStatus = ({ status }: NTPStatus) => {
|
||||
const ntpStatus = ({ status }: NTPStatusType) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return LL.NOT_ENABLED();
|
||||
@@ -201,7 +199,7 @@ const NTPStatusForm: FC = () => {
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
{me.admin && data && !isNtpActive(data) && (
|
||||
{data && !isNtpActive(data) && (
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
|
||||
@@ -216,11 +214,7 @@ const NTPStatusForm: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF('NTP')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NTPStatusForm;
|
||||
export default NTPStatus;
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { useContext } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import NTPSettingsForm from './NTPSettingsForm';
|
||||
import NTPStatusForm from './NTPStatusForm';
|
||||
import NTPSettings from './NTPSettings';
|
||||
import NTPStatus from './NTPStatus';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
@@ -14,26 +12,18 @@ const NetworkTime: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle('NTP');
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/ntp/status" label={LL.STATUS_OF('NTP')} />
|
||||
<Tab value="/ntp/settings" label={LL.SETTINGS_OF('NTP')} disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF('NTP')} />
|
||||
<Tab value="status" label={LL.STATUS_OF('NTP')} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<NTPStatusForm />} />
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<NTPSettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate replace to="/ntp/status" />} />
|
||||
<Route path="status" element={<NTPStatus />} />
|
||||
<Route path="settings" element={<NTPSettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { OTASettings } from 'types';
|
||||
import type { OTASettingsType } from 'types';
|
||||
import * as SystemApi from 'api/system';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
BlockNavigation,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
@@ -23,7 +24,7 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
|
||||
|
||||
const OTASettingsForm: FC = () => {
|
||||
const OTASettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saveData,
|
||||
@@ -35,7 +36,7 @@ const OTASettingsForm: FC = () => {
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
errorMessage
|
||||
} = useRest<OTASettings>({
|
||||
} = useRest<OTASettingsType>({
|
||||
read: SystemApi.readOTASettings,
|
||||
update: SystemApi.updateOTASettings
|
||||
});
|
||||
@@ -61,6 +62,8 @@ const OTASettingsForm: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutTitle('OTA');
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
@@ -117,11 +120,11 @@ const OTASettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default OTASettingsForm;
|
||||
export default OTASettings;
|
||||
@@ -14,9 +14,9 @@ import { useContext, useState } from 'react';
|
||||
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import GenerateToken from './GenerateToken';
|
||||
import UserForm from './UserForm';
|
||||
import User from './User';
|
||||
import type { FC } from 'react';
|
||||
import type { SecuritySettings, User } from 'types';
|
||||
import type { SecuritySettingsType, UserType } from 'types';
|
||||
import * as SecurityApi from 'api/security';
|
||||
import { ButtonRow, FormLoader, MessageBox, SectionContent, BlockNavigation } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
@@ -24,13 +24,13 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useRest } from 'utils';
|
||||
import { createUserValidator } from 'validators';
|
||||
|
||||
const ManageUsersForm: FC = () => {
|
||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettings>({
|
||||
const ManageUsers: FC = () => {
|
||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettingsType>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
|
||||
const [user, setUser] = useState<User>();
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [changed, setChanged] = useState<number>(0);
|
||||
const [generatingToken, setGeneratingToken] = useState<string>();
|
||||
@@ -86,7 +86,7 @@ const ManageUsersForm: FC = () => {
|
||||
|
||||
const noAdminConfigured = () => !data.users.find((u) => u.admin);
|
||||
|
||||
const removeUser = (toRemove: User) => {
|
||||
const removeUser = (toRemove: UserType) => {
|
||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||
updateDataValue({ ...data, users });
|
||||
setChanged(changed + 1);
|
||||
@@ -101,7 +101,7 @@ const ManageUsersForm: FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const editUser = (toEdit: User) => {
|
||||
const editUser = (toEdit: UserType) => {
|
||||
setCreating(false);
|
||||
setUser({ ...toEdit });
|
||||
};
|
||||
@@ -219,7 +219,7 @@ const ManageUsersForm: FC = () => {
|
||||
</Box>
|
||||
|
||||
<GenerateToken username={generatingToken} onClose={closeGenerateToken} />
|
||||
<UserForm
|
||||
<User
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
creating={creating}
|
||||
@@ -232,11 +232,11 @@ const ManageUsersForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.MANAGE_USERS()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageUsersForm;
|
||||
export default ManageUsers;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||
import ManageUsersForm from './ManageUsersForm';
|
||||
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||
import ManageUsers from './ManageUsers';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
|
||||
@@ -17,13 +17,13 @@ const Security: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/security/users" label={LL.MANAGE_USERS()} />
|
||||
<Tab value="/security/settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
|
||||
<Tab value="users" label={LL.MANAGE_USERS()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="users" element={<ManageUsersForm />} />
|
||||
<Route path="settings" element={<SecuritySettingsForm />} />
|
||||
<Route path="*" element={<Navigate replace to="/security/users" />} />
|
||||
<Route path="users" element={<ManageUsers />} />
|
||||
<Route path="settings" element={<SecuritySettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useContext, useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { SecuritySettings } from 'types';
|
||||
import type { SecuritySettingsType } from 'types';
|
||||
import * as SecurityApi from 'api/security';
|
||||
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
|
||||
|
||||
const SecuritySettingsForm: FC = () => {
|
||||
const SecuritySettings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -29,7 +29,7 @@ const SecuritySettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<SecuritySettings>({
|
||||
} = useRest<SecuritySettingsType>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
@@ -96,11 +96,11 @@ const SecuritySettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettingsForm;
|
||||
export default SecuritySettings;
|
||||
@@ -8,7 +8,7 @@ import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { User } from 'types';
|
||||
import type { UserType } from 'types';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
@@ -19,14 +19,14 @@ interface UserFormProps {
|
||||
creating: boolean;
|
||||
validator: Schema;
|
||||
|
||||
user?: User;
|
||||
setUser: React.Dispatch<React.SetStateAction<User | undefined>>;
|
||||
user?: UserType;
|
||||
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>;
|
||||
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
}
|
||||
|
||||
const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
|
||||
const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValue(setUser);
|
||||
@@ -104,4 +104,4 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
);
|
||||
};
|
||||
|
||||
export default UserForm;
|
||||
export default User;
|
||||
154
interface/src/framework/system/ESPSystemStatus.tsx
Normal file
154
interface/src/framework/system/ESPSystemStatus.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
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 { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
const ESPSystemStatus: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.STATUS_OF('ESP32'));
|
||||
|
||||
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: '#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.HEAP()}
|
||||
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
|
||||
/>
|
||||
</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;
|
||||
@@ -1,53 +1,42 @@
|
||||
import { Tab } from '@mui/material';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, type FC } from 'react';
|
||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||
import OTASettingsForm from './OTASettingsForm';
|
||||
import SystemLog from './SystemLog';
|
||||
import SystemStatusForm from './SystemStatusForm';
|
||||
import UploadFileForm from './UploadFileForm';
|
||||
import type { FC } from 'react';
|
||||
import SystemStatus from './SystemStatus';
|
||||
|
||||
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import SystemActivity from 'project/SystemActivity';
|
||||
|
||||
const System: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.SYSTEM(0));
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const { routerTab } = useRouterTab();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="/system/status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
||||
<Tab value="/system/log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
||||
<Tab value="/system/ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />
|
||||
<Tab value="/system/upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />
|
||||
<Tab value="status" label={LL.STATUS_OF('')} />
|
||||
<Tab value="activity" label={LL.ACTIVITY()} />
|
||||
<Tab disabled={!me.admin} value="log" label={me.admin ? LL.LOG_OF('') : ''} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<SystemStatusForm />} />
|
||||
<Route path="log" element={<SystemLog />} />
|
||||
<Route path="status" element={<SystemStatus />} />
|
||||
<Route path="activity" element={<SystemActivity />} />
|
||||
<Route
|
||||
path="ota"
|
||||
path="log"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<OTASettingsForm />
|
||||
<SystemLog />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="upload"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<UploadFileForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate replace to="/system/status" />} />
|
||||
<Route path="*" element={<Navigate replace to="status" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { addAccessTokenParameter } from 'api/authentication';
|
||||
import { EVENT_SOURCE_ROOT } from 'api/endpoints';
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } from 'components';
|
||||
import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation, useLayoutTitle } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { LogLevel } from 'types';
|
||||
@@ -50,6 +50,8 @@ const levelLabel = (level: LogLevel) => {
|
||||
const SystemLog: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.LOG_OF(''));
|
||||
|
||||
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||
useRest<LogSettings>({
|
||||
read: SystemApi.readLogSettings,
|
||||
@@ -232,7 +234,7 @@ const SystemLog: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.LOG_OF(LL.SYSTEM(2))} titleGutter id="log-window">
|
||||
<SectionContent id="log-window">
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
|
||||
297
interface/src/framework/system/SystemStatus.tsx
Normal file
297
interface/src/framework/system/SystemStatus.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CastIcon from '@mui/icons-material/Cast';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import TimerIcon from '@mui/icons-material/Timer';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, type FC, useState } from 'react';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as EMSESP from 'project/api';
|
||||
import { busConnectionStatus } from 'project/types';
|
||||
import { NTPSyncStatus } from 'types';
|
||||
|
||||
const SystemStatus: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.STATUS_OF(''));
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [confirmScan, setConfirmScan] = useState<boolean>(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 activeHighlight = (value: boolean) => (value ? theme.palette.success.main : theme.palette.info.main);
|
||||
|
||||
const scan = async () => {
|
||||
await scanDevices()
|
||||
.then(() => {
|
||||
toast.info(LL.SCANNING() + '...');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
setConfirmScan(false);
|
||||
};
|
||||
|
||||
const renderScanDialog = () => (
|
||||
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}>
|
||||
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
|
||||
{LL.SCAN()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: busStatusHighlight(), color: 'white' }}>
|
||||
<DirectionsBusIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus()} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={TimerIcon}
|
||||
bgcolor="#c5572c"
|
||||
label={LL.UPTIME()}
|
||||
text={formatDurationSec(data.uptime)}
|
||||
/>
|
||||
<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" />
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={BuildIcon}
|
||||
bgcolor="#134ba2"
|
||||
label={LL.EMS_ESP_VER()}
|
||||
text={data.emsesp_version}
|
||||
to="/settings/upload"
|
||||
/>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
{/* TODO: translate */}
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={MemoryIcon}
|
||||
bgcolor="#68374d"
|
||||
label="System Memory"
|
||||
text={formatNumber(data.free_heap) + ' KB'}
|
||||
to="/settings/espsystemstatus"
|
||||
/>
|
||||
<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="/settings/mqtt/status"
|
||||
/>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor={ntpStatusHighlight()}
|
||||
label={LL.STATUS_OF('NTP')}
|
||||
text={ntpStatus()}
|
||||
to="/settings/ntp/status"
|
||||
/>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={CastIcon}
|
||||
bgcolor={activeHighlight(data.ota_status)}
|
||||
label={LL.STATUS_OF('OTA')}
|
||||
text={data.ota_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||
to="/settings/ota"
|
||||
/>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListMenuItem
|
||||
disabled={!me.admin}
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor={activeHighlight(data.ota_status)}
|
||||
label={LL.ACCESS_POINT(0)}
|
||||
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||
to="/settings/ap/status"
|
||||
/>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
|
||||
{renderScanDialog()}
|
||||
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
@@ -1,362 +0,0 @@
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
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 PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
|
||||
import SdStorageIcon from '@mui/icons-material/SdStorage';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import TimerIcon from '@mui/icons-material/Timer';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
import SystemStatusVersionDialog from './SystemStatusVersionDialog';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
const SystemStatusForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
const [versionDialogOpen, setVersionDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
|
||||
|
||||
const restart = async () => {
|
||||
setProcessing(true);
|
||||
await restartCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const factoryReset = async () => {
|
||||
setProcessing(true);
|
||||
await factoryResetCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmFactoryReset(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const partition = async () => {
|
||||
setProcessing(true);
|
||||
await partitionCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.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={restart}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
{data?.has_loader && (
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={partition}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
>
|
||||
EMS-ESP-Loader
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const renderFactoryResetDialog = () => (
|
||||
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(false)}
|
||||
disabled={processing}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={factoryReset}
|
||||
disabled={processing}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<BuildIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.EMS_ESP_VER()} secondary={data.emsesp_version} />
|
||||
<Button color="primary" onClick={() => setVersionDialogOpen(true)}>
|
||||
{LL.VERSION_CHECK(0)}
|
||||
</Button>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<TimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.UPTIME()} secondary={data.uptime} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DevicesIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="SDK" secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<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>
|
||||
<MemoryIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.HEAP()}
|
||||
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
|
||||
/>
|
||||
</ListItem>
|
||||
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
||||
<>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
{me.admin && (
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => setConfirmRestart(true)}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(true)}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{renderRestartDialog()}
|
||||
{renderFactoryResetDialog()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF(LL.SYSTEM(1))} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
{data && (
|
||||
<SystemStatusVersionDialog
|
||||
open={versionDialogOpen}
|
||||
onClose={() => setVersionDialogOpen(false)}
|
||||
version={data.emsesp_version}
|
||||
platform={data.esp_platform}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatusForm;
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Link, Typography } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import MessageBox from 'components/MessageBox';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
type SystemStatusVersionDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
version: string;
|
||||
platform: string;
|
||||
};
|
||||
|
||||
const SystemStatusVersionDialog = ({ open, onClose, version, platform }: SystemStatusVersionDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const { send: getLatestVersion, data: latestVersion } = useRequest(SystemApi.getStableVersion, {
|
||||
immediate: false,
|
||||
force: true
|
||||
});
|
||||
const { send: getLatestDevVersion, data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
|
||||
immediate: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||
|
||||
const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
||||
const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||
|
||||
const uploadURL = window.location.origin + '/system/upload';
|
||||
|
||||
const connected = latestVersion && latestDevVersion;
|
||||
|
||||
const getVersions = useCallback(async () => {
|
||||
await getLatestVersion();
|
||||
await getLatestDevVersion();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
void getVersions();
|
||||
}
|
||||
}, [getVersions, open]);
|
||||
|
||||
const getBinURL = (v: string) => 'EMS-ESP-' + v.replaceAll('.', '_') + '-' + platform.replaceAll('-', '_') + '.bin';
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={onClose}>
|
||||
<DialogTitle>{LL.VERSION_CHECK(1)}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<MessageBox my={0} level="info" message={LL.VERSION_ON() + ' ' + version + ' (' + platform + ')'} />
|
||||
{latestVersion && (
|
||||
<Box mt={2} mb={2}>
|
||||
{LL.THE_LATEST()} <b>{LL.OFFICIAL()}</b> {LL.RELEASE_IS()} <b>{latestVersion}</b>
|
||||
(
|
||||
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link
|
||||
target="_blank"
|
||||
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)}
|
||||
color="primary"
|
||||
>
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
{latestDevVersion && (
|
||||
<Box mt={2} mb={2}>
|
||||
{LL.THE_LATEST()} <b>{LL.DEVELOPMENT()}</b> {LL.RELEASE_IS()}
|
||||
<b>{latestDevVersion}</b>
|
||||
(
|
||||
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion)} color="primary">
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
{connected && (
|
||||
<Box color="warning.main" mt={2}>
|
||||
<Typography variant="body2">
|
||||
{LL.USE()}
|
||||
<Link href={uploadURL} color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Link>
|
||||
{LL.SYSTEM_APPLY_FIRMWARE()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{!connected && <MessageBox my={2} level="warning" message="No internet connection" />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={onClose} color="secondary">
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatusVersionDialog;
|
||||
308
interface/src/framework/system/UploadDownload.tsx
Normal file
308
interface/src/framework/system/UploadDownload.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { Typography, Button, Box, Link } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, type FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as EMSESP from 'project/api';
|
||||
|
||||
const UploadDownload: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
const [md5, setMd5] = useState<string>();
|
||||
|
||||
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data) => EMSESP.API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true });
|
||||
|
||||
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
|
||||
immediate: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
|
||||
immediate: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||
|
||||
const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
||||
const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||
|
||||
const getBinURL = (v: string) =>
|
||||
'EMS-ESP-' + v.replaceAll('.', '_') + '-' + data.esp_platform.replaceAll('-', '_') + '.bin';
|
||||
|
||||
const {
|
||||
loading: isUploading,
|
||||
uploading: progress,
|
||||
send: sendUpload,
|
||||
onSuccess: onSuccessUpload,
|
||||
abort: cancelUpload
|
||||
} = useRequest(SystemApi.uploadFile, {
|
||||
immediate: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
onSuccessUpload(({ data }: any) => {
|
||||
if (data) {
|
||||
setMd5(data.md5);
|
||||
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
|
||||
} else {
|
||||
setRestarting(true);
|
||||
}
|
||||
});
|
||||
|
||||
const startUpload = async (files: File[]) => {
|
||||
await sendUpload(files[0]).catch((err) => {
|
||||
if (err.message === 'The user aborted a request') {
|
||||
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
|
||||
} else if (err.message === 'Network Error') {
|
||||
toast.warning('Invalid file extension or incompatible bin file');
|
||||
} else {
|
||||
toast.error(err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
anchor.download = 'emsesp_' + endpoint;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(anchor.href);
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
};
|
||||
|
||||
onSuccessGetSettings((event) => {
|
||||
saveFile(event.data, 'settings.json');
|
||||
});
|
||||
onSuccessGetCustomizations((event) => {
|
||||
saveFile(event.data, 'customizations.json');
|
||||
});
|
||||
onSuccessGetEntities((event) => {
|
||||
saveFile(event.data, 'entities.json');
|
||||
});
|
||||
onSuccessGetSchedule((event) => {
|
||||
saveFile(event.data, 'schedule.json');
|
||||
});
|
||||
onGetAPI((event) => {
|
||||
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt');
|
||||
});
|
||||
|
||||
const downloadSettings = async () => {
|
||||
await getSettings().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadCustomizations = async () => {
|
||||
await getCustomizations().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadEntities = async () => {
|
||||
await getEntities().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadSchedule = async () => {
|
||||
await getSchedule()
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
});
|
||||
};
|
||||
|
||||
const callAPI = async (device: string, entity: string) => {
|
||||
await getAPI({ device, entity, id: 0 }).catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.UPLOAD_DOWNLOAD());
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
||||
{LL.EMS_ESP_VER()}
|
||||
</Typography>
|
||||
<Box p={2} border="2px solid grey" borderRadius={2}>
|
||||
{LL.VERSION_ON() + ' '}
|
||||
<b>{data.emsesp_version}</b> ({data.esp_platform})
|
||||
{latestVersion && (
|
||||
<Box mt={2}>
|
||||
{LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()} <b>{latestVersion}</b>
|
||||
(
|
||||
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link
|
||||
target="_blank"
|
||||
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)}
|
||||
color="primary"
|
||||
>
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
{latestDevVersion && (
|
||||
<Box mt={2}>
|
||||
{LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}
|
||||
<b>{latestDevVersion}</b>
|
||||
(
|
||||
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion)} color="primary">
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">
|
||||
{LL.UPLOAD_TEXT()}
|
||||
<br />
|
||||
<br />
|
||||
{LL.RESTART_TEXT(1)}.
|
||||
</Typography>
|
||||
</Box>
|
||||
{md5 && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
|
||||
{!isUploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.HELP_INFORMATION_4()}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'info')}
|
||||
>
|
||||
{LL.SUPPORT_INFORMATION(0)}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'allvalues')}
|
||||
>
|
||||
All Values
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadSettings}
|
||||
>
|
||||
{LL.SETTINGS_OF('')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadCustomizations}
|
||||
>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadEntities}
|
||||
>
|
||||
{LL.CUSTOM_ENTITIES(0)}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SCHEDULE_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadSchedule}
|
||||
>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default UploadDownload;
|
||||
@@ -1,224 +0,0 @@
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { Typography, Button, Box } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, type FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { SectionContent, SingleUpload } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as EMSESP from 'project/api';
|
||||
|
||||
const UploadFileForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
const [md5, setMd5] = useState<string>();
|
||||
|
||||
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data) => EMSESP.API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const {
|
||||
loading: isUploading,
|
||||
uploading: progress,
|
||||
send: sendUpload,
|
||||
onSuccess: onSuccessUpload,
|
||||
abort: cancelUpload
|
||||
} = useRequest(SystemApi.uploadFile, {
|
||||
immediate: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
onSuccessUpload(({ data }: any) => {
|
||||
if (data) {
|
||||
setMd5(data.md5);
|
||||
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
|
||||
} else {
|
||||
setRestarting(true);
|
||||
}
|
||||
});
|
||||
|
||||
const startUpload = async (files: File[]) => {
|
||||
await sendUpload(files[0]).catch((err) => {
|
||||
if (err.message === 'The user aborted a request') {
|
||||
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
|
||||
} else if (err.message === 'Network Error') {
|
||||
toast.warning('Invalid file extension or incompatible bin file');
|
||||
} else {
|
||||
toast.error(err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
anchor.download = 'emsesp_' + endpoint;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(anchor.href);
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
};
|
||||
|
||||
onSuccessGetSettings((event) => {
|
||||
saveFile(event.data, 'settings.json');
|
||||
});
|
||||
onSuccessGetCustomizations((event) => {
|
||||
saveFile(event.data, 'customizations.json');
|
||||
});
|
||||
onSuccessGetEntities((event) => {
|
||||
saveFile(event.data, 'entities.json');
|
||||
});
|
||||
onSuccessGetSchedule((event) => {
|
||||
saveFile(event.data, 'schedule.json');
|
||||
});
|
||||
onGetAPI((event) => {
|
||||
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt');
|
||||
});
|
||||
|
||||
const downloadSettings = async () => {
|
||||
await getSettings().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadCustomizations = async () => {
|
||||
await getCustomizations().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadEntities = async () => {
|
||||
await getEntities().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadSchedule = async () => {
|
||||
await getSchedule()
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
});
|
||||
};
|
||||
|
||||
const callAPI = async (device: string, entity: string) => {
|
||||
await getAPI({ device, entity, id: 0 }).catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">
|
||||
{LL.UPLOAD_TEXT()}
|
||||
<br />
|
||||
<br />
|
||||
{LL.RESTART_TEXT(1)}.
|
||||
</Typography>
|
||||
</Box>
|
||||
{md5 && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
|
||||
{!isUploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)} {LL.SUPPORT_INFORMATION(1)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.HELP_INFORMATION_4()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'info')}
|
||||
>
|
||||
{LL.SUPPORT_INFORMATION(0)}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'allvalues')}
|
||||
>
|
||||
All Values
|
||||
</Button>
|
||||
|
||||
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)} {LL.SETTINGS(1)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSettings}>
|
||||
{LL.SETTINGS_OF('')}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadCustomizations}>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadEntities}
|
||||
>
|
||||
{LL.CUSTOM_ENTITIES(0)}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SCHEDULE_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSchedule}>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadFileForm;
|
||||
Reference in New Issue
Block a user