Created form to upload new firmware

This commit is contained in:
Pierre HUBERT 2024-10-07 22:04:57 +02:00
parent f4dda44d15
commit cef5b5aa5b
7 changed files with 566 additions and 248 deletions

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,13 @@
"@mui/material": "^6.1.2", "@mui/material": "^6.1.2",
"@mui/x-charts": "^7.19.0", "@mui/x-charts": "^7.19.0",
"@mui/x-date-pickers": "^7.19.0", "@mui/x-date-pickers": "^7.19.0",
"@types/semver": "^7.5.8",
"date-and-time": "^3.6.0", "date-and-time": "^3.6.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.26.2" "react-router-dom": "^6.26.2",
"semver": "^7.6.3"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.11", "@types/react": "^18.3.11",

View File

@ -15,6 +15,7 @@ import { NotFoundRoute } from "./routes/NotFoundRoute";
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute"; import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
import { RelaysListRoute } from "./routes/RelaysListRoute"; import { RelaysListRoute } from "./routes/RelaysListRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { OTARoute } from "./routes/OTARoute";
export function App() { export function App() {
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled) if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
@ -28,6 +29,7 @@ export function App() {
<Route path="devices" element={<DevicesRoute />} /> <Route path="devices" element={<DevicesRoute />} />
<Route path="dev/:id" element={<DeviceRoute />} /> <Route path="dev/:id" element={<DeviceRoute />} />
<Route path="relays" element={<RelaysListRoute />} /> <Route path="relays" element={<RelaysListRoute />} />
<Route path="ota" element={<OTARoute />} />
<Route path="logs" element={<LogsRoute />} /> <Route path="logs" element={<LogsRoute />} />
<Route path="*" element={<NotFoundRoute />} /> <Route path="*" element={<NotFoundRoute />} />
</Route> </Route>

View File

@ -0,0 +1,15 @@
import { APIClient } from "./ApiClient";
export class OTAAPI {
/**
* Get the list of supported OTA platforms
*/
static async SupportedPlatforms(): Promise<Array<string>> {
return (
await APIClient.exec({
method: "GET",
uri: "/ota/supported_platforms",
})
).data;
}
}

View File

@ -0,0 +1,111 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
InputLabel,
MenuItem,
Select,
styled,
} from "@mui/material";
import React from "react";
import { TextInput } from "../widgets/forms/TextInput";
import { SemVer } from "semver";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});
export function UploadUpdateDialog(p: {
platforms: string[];
onClose: () => void;
onCreated: () => void;
}): React.ReactElement {
const [platform, setPlatform] = React.useState<string | undefined>();
const [version, setVersion] = React.useState<string | undefined>();
const [file, setFile] = React.useState<File | undefined>();
return (
<Dialog open={true} onClose={p.onClose}>
<DialogTitle>Submit a new update</DialogTitle>
<DialogContent>
<DialogContentText>
You can upload a new firmware using this form.
</DialogContentText>
<br />
<FormControl fullWidth>
<InputLabel>Platform</InputLabel>
<Select
label="Platform"
value={platform}
onChange={(e) => setPlatform(e.target.value)}
variant="standard"
>
{p.platforms.map((p) => (
<MenuItem key={p} value={p}>
{p}
</MenuItem>
))}
</Select>
</FormControl>
<br />
<br />
<TextInput
editable
label="Version"
helperText="The version shall follow semantics requirements"
value={version}
onValueChange={setVersion}
checkValue={(v) => {
try {
new SemVer(v, { loose: false });
return true;
} catch (e) {
console.error(e);
return false;
}
}}
/>
<br />
<Button
fullWidth
component="label"
role={undefined}
variant={file ? "contained" : "outlined"}
tabIndex={-1}
startIcon={<CloudUploadIcon />}
>
Upload file
<VisuallyHiddenInput
type="file"
onChange={(event) =>
setFile(
(event.target.files?.length ?? 0) > 0
? event.target.files![0]
: undefined
)
}
multiple
/>
</Button>
</DialogContent>
<DialogActions>
<Button onClick={p.onClose}>Cancel</Button>
<Button type="submit">Subscribe</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -0,0 +1,58 @@
import { IconButton, Tooltip } from "@mui/material";
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { UploadUpdateDialog } from "../dialogs/UploadUpdateDialog";
import React from "react";
import { OTAAPI } from "../api/OTAApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
export function OTARoute(): React.ReactElement {
const [list, setList] = React.useState<string[] | undefined>();
const load = async () => {
setList(await OTAAPI.SupportedPlatforms());
};
return (
<AsyncWidget
loadKey={1}
ready={!!list}
load={load}
errMsg="Failed to load OTA screen!"
build={() => <_OTARoute platforms={list!} />}
/>
);
}
function _OTARoute(p: { platforms: Array<string> }): React.ReactElement {
const [showUploadDialog, setShowUploadDialog] = React.useState(false);
const reload = async () => {
/*todo*/
};
return (
<SolarEnergyRouteContainer
label="OTA"
actions={
<>
<Tooltip title="Upload a new update">
<IconButton onClick={() => setShowUploadDialog(true)}>
<FileUploadIcon />
</IconButton>
</Tooltip>
</>
}
>
{showUploadDialog && (
<UploadUpdateDialog
platforms={p.platforms}
onClose={() => setShowUploadDialog(false)}
onCreated={() => {
setShowUploadDialog(false);
reload();
}}
/>
)}
</SolarEnergyRouteContainer>
);
}

View File

@ -2,6 +2,7 @@ import {
mdiChip, mdiChip,
mdiElectricSwitch, mdiElectricSwitch,
mdiHome, mdiHome,
mdiMonitorArrowDown,
mdiNewBox, mdiNewBox,
mdiNotebookMultiple, mdiNotebookMultiple,
} from "@mdi/js"; } from "@mdi/js";
@ -41,6 +42,11 @@ export function SolarEnergyNavList(): React.ReactElement {
uri="/relays" uri="/relays"
icon={<Icon path={mdiElectricSwitch} size={1} />} icon={<Icon path={mdiElectricSwitch} size={1} />}
/> />
<NavLink
label="OTA"
uri="/ota"
icon={<Icon path={mdiMonitorArrowDown} size={1} />}
/>
<NavLink <NavLink
label="Logging" label="Logging"
uri="/logs" uri="/logs"