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/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",
|
||||||
|
@ -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>
|
||||||
|
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,
|
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"
|
||||||
|
Loading…
Reference in New Issue
Block a user