Created form to upload new firmware
This commit is contained in:
		
							
								
								
									
										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"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user