Created form to upload new firmware
This commit is contained in:
parent
f4dda44d15
commit
cef5b5aa5b
618
central_frontend/package-lock.json
generated
618
central_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,11 +19,13 @@
|
||||
"@mui/material": "^6.1.2",
|
||||
"@mui/x-charts": "^7.19.0",
|
||||
"@mui/x-date-pickers": "^7.19.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"date-and-time": "^3.6.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^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": {
|
||||
"@types/react": "^18.3.11",
|
||||
|
@ -15,6 +15,7 @@ import { NotFoundRoute } from "./routes/NotFoundRoute";
|
||||
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
|
||||
import { RelaysListRoute } from "./routes/RelaysListRoute";
|
||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||
import { OTARoute } from "./routes/OTARoute";
|
||||
|
||||
export function App() {
|
||||
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
||||
@ -28,6 +29,7 @@ export function App() {
|
||||
<Route path="devices" element={<DevicesRoute />} />
|
||||
<Route path="dev/:id" element={<DeviceRoute />} />
|
||||
<Route path="relays" element={<RelaysListRoute />} />
|
||||
<Route path="ota" element={<OTARoute />} />
|
||||
<Route path="logs" element={<LogsRoute />} />
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
|
15
central_frontend/src/api/OTAApi.ts
Normal file
15
central_frontend/src/api/OTAApi.ts
Normal 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;
|
||||
}
|
||||
}
|
111
central_frontend/src/dialogs/UploadUpdateDialog.tsx
Normal file
111
central_frontend/src/dialogs/UploadUpdateDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
58
central_frontend/src/routes/OTARoute.tsx
Normal file
58
central_frontend/src/routes/OTARoute.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -2,6 +2,7 @@ import {
|
||||
mdiChip,
|
||||
mdiElectricSwitch,
|
||||
mdiHome,
|
||||
mdiMonitorArrowDown,
|
||||
mdiNewBox,
|
||||
mdiNotebookMultiple,
|
||||
} from "@mdi/js";
|
||||
@ -41,6 +42,11 @@ export function SolarEnergyNavList(): React.ReactElement {
|
||||
uri="/relays"
|
||||
icon={<Icon path={mdiElectricSwitch} size={1} />}
|
||||
/>
|
||||
<NavLink
|
||||
label="OTA"
|
||||
uri="/ota"
|
||||
icon={<Icon path={mdiMonitorArrowDown} size={1} />}
|
||||
/>
|
||||
<NavLink
|
||||
label="Logging"
|
||||
uri="/logs"
|
||||
|
Loading…
Reference in New Issue
Block a user