refactored system settings

This commit is contained in:
2025-09-12 13:28:14 +01:00
parent d03f73f751
commit 7588326cbe
19 changed files with 363 additions and 232 deletions

View File

@@ -1,17 +1,17 @@
export async function handleSystemSave(
deviceName: string,
sntpServer: string,
sntpInterval: number,
timeZone: string
) {
import type { SystemValues } from "../../../types/types";
export async function handleSystemSave(values: SystemValues) {
const payload = {
// Build JSON
id: "GLOBAL--Device",
fields: [
{ property: "propDeviceName", value: deviceName },
{ property: "propSNTPServer", value: sntpServer },
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
{ property: "propLocalTimeZone", value: timeZone },
{ property: "propDeviceName", value: values.deviceName },
{ property: "propSNTPServer", value: values.sntpServer },
{
property: "propSNTPIntervalMinutes",
value: Number(values.sntpInterval),
},
{ property: "propLocalTimeZone", value: values.timeZone },
],
};

View File

@@ -1,54 +1,31 @@
import React from "react";
import { useEffect } from "react"
import Card from "../../UI/Card";
import CardHeader from "../../UI/CardHeader";
import FormGroup from "../components/FormGroup";
import { sendBlobFileUpload } from "./Upload";
import { handleSoftReboot, handleHardReboot } from "./Reboots.tsx";
import { handleSystemRecall, handleSystemSave } from "./SettingSaveRecall.tsx";
import SystemConfigFields from "./SystemConfigFields.tsx";
const SystemCard = () => {
const [deviceName, setDeviceName] = React.useState("");
const [timeZone, setTimeZone] = React.useState("Europe/London (UTC+00:00");
const [sntpServer, setSntpServer] = React.useState("1.uk.pool.ntp.org");
const [sntpInterval, setSntpInterval] = React.useState(60);
const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
const [error, setError] = React.useState("");
useEffect(() => {
(async () => {
const result = await handleSystemRecall(); // returns { deviceName, sntpServer, sntpInterval, timeZone } | null
if (result) {
const {
deviceName: dn,
sntpServer: ss,
sntpInterval: si,
timeZone: tz
} = result;
// useEffect(() => {
// (async () => {
// const result = await handleSystemRecall(); // returns { deviceName, sntpServer, sntpInterval, timeZone } | null
// if (result) {
// const {
// deviceName: dn,
// sntpServer: ss,
// sntpInterval: si,
// timeZone: tz,
// } = result;
setDeviceName(dn ?? "");
setSntpServer(ss ?? "");
setSntpInterval(Number.isFinite(si) ? si : 60);
setTimeZone(tz ?? "UTC (UTC-00)");
}
})();
}, []);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
setSelectedFile(file);
if (!file) {
setError("No file selected.");
return;
}
if (file.size > 8 * 1024 * 1024) {
setError("File is too large (max 8MB).");
setSelectedFile(null);
return
};
setError("");
}
// setDeviceName(dn ?? "");
// setSntpServer(ss ?? "");
// setSntpInterval(Number.isFinite(si) ? si : 60);
// setTimeZone(tz ?? "UTC (UTC-00)");
// }
// })();
// }, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // prevent full page reload
@@ -58,160 +35,27 @@ const SystemCard = () => {
}
setError("");
const result = await sendBlobFileUpload( selectedFile, {
const result = await sendBlobFileUpload(selectedFile, {
timeoutMs: 30000,
fieldName: "upload",
});
// The helper returns a string (either success body or formatted error)
// You can decide how to distinguish. Here, we show it optimistically and let the text speak.
if (result.startsWith("Server returned") || result.startsWith("Timeout") || result.startsWith("HTTP error") || result.startsWith("Unexpected error")) {
if (
result.startsWith("Server returned") ||
result.startsWith("Timeout") ||
result.startsWith("HTTP error") ||
result.startsWith("Unexpected error")
) {
setError(result);
}
};
return (
<Card className="flex flex-col items-center justify-center">
<CardHeader title={"System Config"} />
<div className="flex flex-col gap-4 w-full items-left max-w-md">
<FormGroup>
<label htmlFor="deviceName" className="font-medium whitespace-nowrap md:w-1/2 text-left">Device Name</label>
<div className="flex-1 flex justify-end md:w-2/3">
<input
id="deviceName"
name="deviceName"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter device name"
value={deviceName}
onChange={e => setDeviceName(e.target.value)}
/>
</div>
</FormGroup>
<FormGroup>
<label htmlFor="timeZone" className="font-medium whitespace-nowrap md:w-1/2 text-left">Local Time Zone</label>
<div className="flex-1 flex justify-end md:w-2/3">
<select
id="timeZone"
name="timeZone"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full max-w-xs"
value={timeZone}
onChange={e => setTimeZone(e.target.value)}
>
<option value="">Select Time Zone</option>
<option value="Europe/London (UTC+00)">UTC (UTC+00)</option>
<option value="Africa/Cairo (UTC+02)">Africa/Cairo (UTC+02)</option>
<option value="Africa/Johannesburg (UTC+02)">Africa/Johannesburg (UTC+02)</option>
<option value="Africa/Lagos (UTC+01)">Africa/Lagos (UTC+01)</option>
<option value="Africa/Monrousing (UTC+00)">Africa/Monrousing (UTC+00)</option>
<option value="America/Anchorage (UTC-09)">America/Anchorage (UTC-09)</option>
<option value="America/Chicago (UTC-06)">America/Chicago (UTC-06)</option>
<option value="America/Denver (UTC-07)">America/Denver (UTC-07)</option>
<option value="America/Edmonton (UTC-07)">America/Edmonton (UTC-07)</option>
<option value="America/Jamaica (UTC-05)">America/Jamaica (UTC-05)</option>
<option value="America/Los Angeles (UTC-08)">America/Los Angeles (UTC-08)</option>
<option value="America/Mexico City (UTC-06)">America/Mexico City (UTC-06)</option>
<option value="America/Montreal (UTC-05)">America/Montreal (UTC-05)</option>
<option value="America/New York (UTC-05)">America/New York (UTC-05)</option>
<option value="America/Phoenix (UTC-07)">America/Phoenix (UTC-07)</option>
<option value="America/Puerto Rico (UTC-04)">America/Puerto Rico (UTC-04)</option>
<option value="America/Sao Paulo (UTC-03)">America/Sao Paulo (UTC-03)</option>
<option value="America/Toronto (UTC-05)">America/Toronto (UTC-05)</option>
<option value="America/Vancouver (UTC-08)">America/Vancouver (UTC-08)</option>
<option value="Asia/Hong Kong (UTC+08)">Asia/Hong Kong (UTC+08)</option>
<option value="Asia/Jerusalem (UTC+02)">Asia/Jerusalem (UTC+02)</option>
<option value="Asia/Manila (UTC+08)">Asia/Manila (UTC+08)</option>
<option value="Asia/Seoul (UTC+09)">Asia/Seoul (UTC+09)</option>
<option value="Asia/Tokyo (UTC+09)">Asia/Tokyo (UTC+09)</option>
<option value="Atlantic/Reykjavik (UTC+00)">Atlantic/Reykjavik (UTC+00)</option>
<option value="Australia/Perth (UTC+08)">Australia/Perth (UTC+08)</option>
<option value="Australia/Sydney (UTC+10)">Australia/Sydney (UTC+10)</option>
<option value="Europe/Athens (UTC+02)">Europe/Athens (UTC+02)</option>
<option value="Europe/Berlin (UTC+01)">Europe/Berlin (UTC+01)</option>
<option value="Europe/Brussels (UTC+01)">Europe/Brussels (UTC+01)</option>
<option value="Europe/Copenhagen (UTC+01)">Europe/Copenhagen (UTC+01)</option>
<option value="Europe/London (UTC+00)">Europe/London (UTC+00)</option>
<option value="Europe/Madrid (UTC+01)">Europe/Madrid (UTC+01)</option>
<option value="Europe/Moscow (UTC+04)">Europe/Moscow (UTC+04)</option>
<option value="Europe/Paris (UTC+01)">Europe/Paris (UTC+01)</option>
<option value="Europe/Prague (UTC+01)">Europe/Prague (UTC+01)</option>
<option value="Europe/Rome (UTC+01)">Europe/Rome (UTC+01)</option>
<option value="Europe/Warsaw (UTC+01)">Europe/Warsaw (UTC+01)</option>
<option value="Pacific/Guam (UTC+10)">Pacific/Guam (UTC+10)</option>
<option value="Pacific/Honolulu (UTC-10)">Pacific/Honolulu (UTC-10)</option>
</select>
</div>
</FormGroup>
<FormGroup>
<label htmlFor="sntpServer" className="font-medium whitespace-nowrap md:w-1/2 text-left">SNTP Server</label>
<div className="flex-1 flex justify-end md:w-2/3">
<input
id="sntpServer"
name="sntpServer"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter SNTP server address"
value={sntpServer}
onChange={e => setSntpServer(e.target.value)}
/>
</div>
</FormGroup>
<FormGroup>
<label htmlFor="sntpInterval" className="font-medium whitespace-nowrap md:w-1/2 text-left">SNTP Interval minutes</label>
<div className="flex-1 flex justify-end md:w-2/3">
<input
id="sntpInterval"
name="sntpInterval"
type="number"
min={1}
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
value={sntpInterval}
onChange={e => setSntpInterval(Number(e.target.value))}
/>
</div>
</FormGroup>
<button
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
onClick={() => handleSystemSave(deviceName, sntpServer, sntpInterval, timeZone)}
>
Save System Settings
</button>
<div className="py-8 w-full">
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-2 w-full">
<FormGroup>
<div className="flex-1 flex justify-end md:w-2/3">
<input
type="file"
name="softwareUpdate"
id="softwareUpdate"
className="file:px-10 file:border file:border-gray-500 file:rounded-lg file:bg-blue-800 file:mr-5 w-full max-w-xs"
onChange={handleFileChange}
/>
</div>
</FormGroup>
<button
type="submit"
className="w-full max-w-md text-white bg-[#26B170] hover:bg-green-700 font-small rounded-lg text-sm px-2 py-2.5 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedFile}
>
Upload Software Update
</button>
{error && <p className="text-red-500 text-sm">{error}</p>}
</form>
</div>
<button
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full max-w-md"
onClick={handleSoftReboot}
>
Software Reboot
</button>
<button
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full max-w-md"
onClick={handleHardReboot}
>
Hardware Reboot
</button>
</div>
<Card>
<CardHeader title={"System Config"} />
<SystemConfigFields />
</Card>
);
};

View File

@@ -0,0 +1,151 @@
import { Formik, Field, Form } from "formik";
import FormGroup from "../components/FormGroup";
import { handleSoftReboot, handleHardReboot } from "./Reboots";
import { handleSystemSave } from "./SettingSaveRecall";
import { timezones } from "./timezones";
import SystemFileUpload from "./SystemFileUpload";
import type { SystemValues, SystemValuesErrors } from "../../../types/types";
const SystemConfigFields = () => {
const initialvalues: SystemValues = {
deviceName: "",
timeZone: "",
sntpServer: "",
sntpInterval: 60,
softwareUpdate: null,
};
const handleSubmit = (values: SystemValues) => handleSystemSave(values);
const validateValues = (values: SystemValues) => {
const errors: SystemValuesErrors = {};
const interval = Number(values.sntpInterval);
if (!values.deviceName) errors.deviceName = "Required";
if (!values.timeZone) errors.timeZone = "Required";
if (isNaN(interval) || interval <= 0)
errors.sntpInterval = "Cannot be less than 0";
if (!values.sntpServer) errors.sntpServer = "Required";
return errors;
};
return (
<Formik
initialValues={initialvalues}
onSubmit={handleSubmit}
validate={validateValues}
>
{({ values, errors, touched }) => (
<Form className="flex flex-col space-y-5">
<FormGroup>
<label
htmlFor="deviceName"
className="font-medium whitespace-nowrap md:w-1/2 text-left"
>
Device Name
</label>
{touched.deviceName && errors.deviceName && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.deviceName}
</small>
)}
<Field
id="deviceName"
name="deviceName"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter device name"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="timeZone"
className="font-medium whitespace-nowrap md:w-1/2 text-left"
>
Local Time Zone
</label>
{touched.timeZone && errors.timeZone && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.timeZone}
</small>
)}
<Field
id="timeZone"
name="timeZone"
as="select"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full max-w-xs"
>
{timezones.map((timezone) => (
<option value={timezone.value} key={timezone.label}>
{timezone.label}
</option>
))}
</Field>
</FormGroup>
<FormGroup>
<label
htmlFor="sntpServer"
className="font-medium whitespace-nowrap md:w-1/2 text-left"
>
SNTP Server
</label>
{touched.sntpServer && errors.sntpServer && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.sntpServer}
</small>
)}
<Field
id="sntpServer"
name="sntpServer"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter SNTP server address"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="sntpInterval"
className="font-medium whitespace-nowrap md:w-1/2 text-left"
>
SNTP Interval minutes
</label>
{touched.sntpInterval && errors.sntpInterval && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.sntpInterval}
</small>
)}
<Field
id="sntpInterval"
name="sntpInterval"
type="number"
min={1}
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
/>
</FormGroup>
<button
type="submit"
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
>
Save System Settings
</button>
<SystemFileUpload
name={"softwareUpdate"}
selectedFile={values.softwareUpdate}
/>
<button
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full max-w-md"
onClick={handleSoftReboot}
>
Software Reboot
</button>
<button
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full max-w-md"
onClick={handleHardReboot}
>
Hardware Reboot
</button>
</Form>
)}
</Formik>
);
};
export default SystemConfigFields;

View File

@@ -0,0 +1,49 @@
import { useFormikContext } from "formik";
import FormGroup from "../components/FormGroup";
import { toast } from "sonner";
type SystemFileUploadProps = {
name: string;
selectedFile: File | null | undefined;
};
const SystemFileUpload = ({ name, selectedFile }: SystemFileUploadProps) => {
const { setFieldValue } = useFormikContext();
const handleFileUploadClick = () => console.log(selectedFile);
return (
<div className="py-8 w-full">
<FormGroup>
<div className="flex-1 flex justify-end md:w-2/3">
<input
type="file"
name="softwareUpdate"
id="softwareUpdate"
className="file:px-10 file:border file:border-gray-500 file:rounded-lg file:bg-blue-800 file:mr-5 w-full max-w-xs"
onChange={(event) => {
const file = event.currentTarget.files?.[0];
if (!file) {
toast.error("No File selected");
return;
}
if (file?.size > 8 * 1024 * 1024)
toast.error("File is too large (max 8MB).");
setFieldValue(name, file);
}}
/>
</div>
</FormGroup>
<button
type="button"
className="w-full max-w-md text-white bg-[#26B170] hover:bg-green-700 font-small rounded-lg text-sm px-2 py-2.5 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedFile}
onClick={handleFileUploadClick}
>
Upload Software Update
</button>
</div>
);
};
export default SystemFileUpload;

View File

@@ -0,0 +1,58 @@
export const timezones = [
{ value: "Europe/London (UTC+00)", label: "Select Time Zone" },
{ value: "Europe/London (UTC+00)", label: "UTC (UTC+00)" },
{ value: "Africa/Cairo (UTC+02)", label: "Africa/Cairo (UTC+02)" },
{
value: "Africa/Johannesburg (UTC+02)",
label: "Africa/Johannesburg (UTC+02)",
},
{ value: "Africa/Lagos (UTC+01)", label: "Africa/Lagos (UTC+01)" },
{ value: "Africa/Monrousing (UTC+00)", label: "Africa/Monrousing (UTC+00)" },
{ value: "America/Anchorage (UTC-09)", label: "America/Anchorage (UTC-09)" },
{ value: "America/Chicago (UTC-06)", label: "America/Chicago (UTC-06)" },
{ value: "America/Denver (UTC-07)", label: "America/Denver (UTC-07)" },
{ value: "America/Edmonton (UTC-07)", label: "America/Edmonton (UTC-07)" },
{ value: "America/Jamaica (UTC-05)", label: "America/Jamaica (UTC-05)" },
{
value: "America/Los Angeles (UTC-08)",
label: "America/Los Angeles (UTC-08)",
},
{
value: "America/Mexico City (UTC-06)",
label: "America/Mexico City (UTC-06)",
},
{ value: "America/Montreal (UTC-05)", label: "America/Montreal (UTC-05)" },
{ value: "America/New York (UTC-05)", label: "America/New York (UTC-05)" },
{ value: "America/Phoenix (UTC-07)", label: "America/Phoenix (UTC-07)" },
{
value: "America/Puerto Rico (UTC-04)",
label: "America/Puerto Rico (UTC-04)",
},
{ value: "America/Sao Paulo (UTC-03)", label: "America/Sao Paulo (UTC-03)" },
{ value: "America/Toronto (UTC-05)", label: "America/Toronto (UTC-05)" },
{ value: "America/Vancouver (UTC-08)", label: "America/Vancouver (UTC-08)" },
{ value: "Asia/Hong Kong (UTC+08)", label: "Asia/Hong Kong (UTC+08)" },
{ value: "Asia/Jerusalem (UTC+02)", label: "Asia/Jerusalem (UTC+02)" },
{ value: "Asia/Manila (UTC+08)", label: "Asia/Manila (UTC+08)" },
{ value: "Asia/Seoul (UTC+09)", label: "Asia/Seoul (UTC+09)" },
{ value: "Asia/Tokyo (UTC+09)", label: "Asia/Tokyo (UTC+09)" },
{
value: "Atlantic/Reykjavik (UTC+00)",
label: "Atlantic/Reykjavik (UTC+00)",
},
{ value: "Australia/Perth (UTC+08)", label: "Australia/Perth (UTC+08)" },
{ value: "Australia/Sydney (UTC+10)", label: "Australia/Sydney (UTC+10)" },
{ value: "Europe/Athens (UTC+02)", label: "Europe/Athens (UTC+02)" },
{ value: "Europe/Berlin (UTC+01)", label: "Europe/Berlin (UTC+01)" },
{ value: "Europe/Brussels (UTC+01)", label: "Europe/Brussels (UTC+01)" },
{ value: "Europe/Copenhagen (UTC+01)", label: "Europe/Copenhagen (UTC+01)" },
{ value: "Europe/London (UTC+00)", label: "Europe/London (UTC+00)" },
{ value: "Europe/Madrid (UTC+01)", label: "Europe/Madrid (UTC+01)" },
{ value: "Europe/Moscow (UTC+04)", label: "Europe/Moscow (UTC+04)" },
{ value: "Europe/Paris (UTC+01)", label: "Europe/Paris (UTC+01)" },
{ value: "Europe/Prague (UTC+01)", label: "Europe/Prague (UTC+01)" },
{ value: "Europe/Rome (UTC+01)", label: "Europe/Rome (UTC+01)" },
{ value: "Europe/Warsaw (UTC+01)", label: "Europe/Warsaw (UTC+01)" },
{ value: "Pacific/Guam (UTC+10)", label: "Pacific/Guam (UTC+10)" },
{ value: "Pacific/Honolulu (UTC-10)", label: "Pacific/Honolulu (UTC-10)" },
];