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