diff --git a/src/components/Contexts/ManifestsContext.jsx b/src/components/Contexts/ManifestsContext.jsx index 8695122..d10c1f3 100644 --- a/src/components/Contexts/ManifestsContext.jsx +++ b/src/components/Contexts/ManifestsContext.jsx @@ -83,10 +83,10 @@ export const ManifestsProvider = ({ children }) => { const migrateManifest = async (package_id, token) => { let result; - try{ + try { setBusy(true) result = await api.migrateManifest(package_id, token) - if(result.error) + if (result.error) throw new Error(`failed to migrate manifest. ${result.message}`); } finally { setBusy(false) @@ -116,7 +116,7 @@ export const ManifestsProvider = ({ children }) => { const validateManifestUpdate = (data) => { if (data == null) throw new Error("No manifest data"); - if (data.name == null || data.name.length>255) throw new Error("Invalid manifest name"); + if (data.name == null || data.name.length > 255) throw new Error("Invalid manifest name"); if (data.type == null || !["forced", "standard"].includes(data.type)) { throw new Error("Invalid manifest type"); diff --git a/src/components/Controls/DropDownList/index.jsx b/src/components/Controls/DropDownList/index.jsx index d9bf5ef..d2d3851 100644 --- a/src/components/Controls/DropDownList/index.jsx +++ b/src/components/Controls/DropDownList/index.jsx @@ -1,10 +1,11 @@ import { FormControl, InputLabel, Select } from "@material-ui/core"; -export const DropDownList = ({data, label, value, labelField, valueField, onChange, classes, ...others}) => { +export const DropDownList = ({ data, label, value, labelField, valueField, onChange, classes, fullWidth, ...others }) => { return ( {label} @@ -16,9 +17,9 @@ export const DropDownList = ({data, label, value, labelField, valueField, onChan onChange={onChange} {...others} > - {data && data.map((item, index) => ( - - ))} + {data && data.map((item, index) => ( + + ))} ); diff --git a/src/components/Manifest/Deploy/Configure.jsx b/src/components/Manifest/Deploy/Configure.jsx new file mode 100644 index 0000000..f22dece --- /dev/null +++ b/src/components/Manifest/Deploy/Configure.jsx @@ -0,0 +1,230 @@ +import { useEffect, useState } from "react"; +import { + Box, + FormControl, + IconButton, + Input, + InputLabel, + FormHelperText, + Modal, + TextField, + Typography, + Button, +} from "@material-ui/core"; +import CloseIcon from '@mui/icons-material/Close'; +import SendIcon from "@material-ui/icons/Send"; + +import { SELECT_VERSION } from "../../Contexts/CarUpdatesContext"; +import { useManifestsContext } from "../../Contexts/ManifestsContext"; +import { useStatusContext } from "../../Contexts/StatusContext"; +import { useUserContext } from "../../Contexts/UserContext"; + +import { DropDownList } from "../../Controls/DropDownList"; + +const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + boxShadow: 24, + borderRadius: 8, + p: 4, +}; + +/** + * Checks several required fields + * @param {Manifest} manifest + * @returns []bool representing each valid field + */ +const validateManifestFields = (manifest = {}) => [ + manifest.sums && manifest.sums !== SELECT_VERSION, + manifest.max_attempts > 0, + manifest.update_duration > 0, +]; + +/** + * Returns true if manifests have different values + * @param {Manifest} a + * @param {Manifest} b + * @returns bool + */ +const diffManifestFields = (a = {}, b = {}) => ( + a.release_notes !== b.release_notes || + a.max_attempts !== b.max_attempts || + a.update_duration !== b.update_duration || + a.sums !== b.sums +); + +export default function Configure({ + manifest = {}, + classes, + versions = [SELECT_VERSION], + disabled = true, + submit = () => { }, +}) { + const { updateManifest } = useManifestsContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + } = useUserContext(); + const { setMessage } = useStatusContext(); + + const [open, setOpen] = useState(false); + const [localManifest, setLocalManifest] = useState(structuredClone(manifest)); + + const [satisfiesRequiredFields, setSatisfiesRequiredFields] = useState(validateManifestFields(localManifest)); + const needsConfiguration = satisfiesRequiredFields.some(valid => !valid); + + const handleOpen = () => { + if (needsConfiguration) { + setOpen(true); + } else { + onSubmit(); // skip openning modal, if fully configured + } + } + const handleClose = () => setOpen(false); + + const handleManifestField = (field, value) => { + if (!field || !value) { + return; + } + setLocalManifest((manifest) => { + manifest[field] = value; + return structuredClone(manifest); + }); + } + + const onSubmit = async () => { + handleClose(); + if (diffManifestFields(manifest, localManifest)) { + try { + localManifest.update_duration = parseInt(localManifest.update_duration); + localManifest.max_attempts = parseInt(localManifest.max_attempts); + + const result = await updateManifest(manifest.id, localManifest, token); + + if (!result || result.error) { + return; + } + + setMessage(`Updated manifest ${manifest.id}`); + } catch (err) { + setMessage(`Failed to update manifest ${manifest.id}`); + } + } + submit(); + } + + useEffect(() => { + setSatisfiesRequiredFields(validateManifestFields(localManifest)); + }, [localManifest, setSatisfiesRequiredFields]) + + return ( + <> + + + + + + + + + + Complete Manifest + + + {manifest.name ? ({manifest.name}) : `This manifest`} is incomplete, and cannot be deployed without all required fields filled out. + +
+ handleManifestField("sums", e.target.value)} + error={!satisfiesRequiredFields[0]} + fullWidth + /> + handleManifestField("max_attempts", value)} + error={!satisfiesRequiredFields[1]} + /> + handleManifestField("update_duration", value)} + error={!satisfiesRequiredFields[2]} + /> + handleManifestField("release_notes", value)} + fullWidth + /> + +
+
+ + ); +} + +function kebab(str) { + return str.replaceAll(" ", "-").toLowerCase(); +} + +function ConfigureInput({ + name, + description, + value = 0, + setValue = () => { }, + error, +}) { + const inputId = `deploy-configure-${kebab(name)}`; + const descriptionId = `deploy-configure-${kebab(name)}-explain`; + + const handleChange = (event) => { + setValue(event.target.value); + } + return ( + + + {name} + + {description} + + + ); +} diff --git a/src/components/Manifest/Deploy/index.jsx b/src/components/Manifest/Deploy/index.jsx index 7bb7abd..ded0190 100644 --- a/src/components/Manifest/Deploy/index.jsx +++ b/src/components/Manifest/Deploy/index.jsx @@ -1,13 +1,11 @@ -import { Button, Checkbox, FormControlLabel, Grid, MenuItem, Switch, Typography } from "@material-ui/core"; -import clsx from "clsx"; -import SendIcon from "@material-ui/icons/Send"; +import { Checkbox, FormControlLabel, Grid, MenuItem, Switch, Typography, Box } from "@material-ui/core"; import React, { useEffect, useState } from "react"; import { Redirect, useParams } from "react-router"; import { logger } from "../../../services/monitoring"; import { LocalDateTimeString } from "../../../utils/dates"; import { Permissions } from "../../../utils/roles"; -import { CarUpdatesProvider, SELECT_VERSION, useCarUpdatesContext } from "../../Contexts/CarUpdatesContext"; +import { CarUpdatesProvider, useCarUpdatesContext } from "../../Contexts/CarUpdatesContext"; import { FleetProvider } from "../../Contexts/FleetContext"; import { ManifestsProvider, useManifestsContext } from "../../Contexts/ManifestsContext"; import { useStatusContext } from "../../Contexts/StatusContext"; @@ -15,11 +13,11 @@ import { useUserContext } from "../../Contexts/UserContext"; import { VehicleProvider } from "../../Contexts/VehicleContext"; import CarSelectionTable from "../../Controls/CarSelectionTable"; import OptionsDropdown from "../../Controls/OptionsDropdown"; -import { DropDownList } from "../../Controls/DropDownList"; import FleetSelectionTable from "../../Controls/FleetSelectionTable"; import { RoleWrap } from "../../Controls/RoleWrap"; import SearchField from "../../Controls/SearchField"; import useStyles from "../../useStyles"; +import Configure from "./Configure"; const CAR_UPDATE = false; const FLEET_UPDATE = true; @@ -28,7 +26,7 @@ const MainForm = () => { const [updateType, setUpdateType] = useState(CAR_UPDATE); const { manifest_id } = useParams(); const { getManifests, manifests, busy } = useManifestsContext(); - const { deployCarUpdates, deployFleetUpdates, getSUMSVersions, versions, updateSUMSVersion } = useCarUpdatesContext(); + const { deployCarUpdates, deployFleetUpdates, getSUMSVersions, versions } = useCarUpdatesContext(); const { groups, providers, @@ -37,15 +35,11 @@ const MainForm = () => { }, } = useUserContext(); const { setMessage, setTitle, setSitePath } = useStatusContext(); - const [manifestName, setManifestName] = useState(""); - const [version, setVersion] = useState(""); - const [sumsVersion, setSUMSersion] = useState(""); - const [createDate, setCreateDate] = useState(""); + const [manifest, setManifest] = useState({}); const [selected, setSelected] = useState([]); const [search, setSearch] = useState(""); const [online, setOnline] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false); - const [softwareVersion, setSoftwareVersion] = useState(SELECT_VERSION); const [redirect, setRedirect] = useState(""); const classes = useStyles(); @@ -86,15 +80,11 @@ const MainForm = () => { } }; - const onSubmit = async (event) => { + const onSubmit = async () => { try { - event.preventDefault(); const data = { manifest_id: parseInt(manifest_id), } - if (sumsVersion.length === 0) { - await updateSUMSVersion(manifest_id, softwareVersion, token); - } if (updateType === CAR_UPDATE) { data.vins = selected; @@ -104,7 +94,7 @@ const MainForm = () => { await deployFleetUpdates(data, token); } setMessage( - `Deployed ${manifestName} ${version} to ${selected.length} cars` + `Deployed ${manifest.name} ${manifest.version} to ${selected.length} cars` ); setRedirect(`/package-status/${manifest_id}`); } catch (e) { @@ -113,65 +103,77 @@ const MainForm = () => { } }; - const getData = async () => { - try { - await getManifests({ id: parseInt(manifest_id) }, token); - await getSUMSVersions(token); - } catch (e) { - setMessage(e.message); - logger.warn(e.stack); + useEffect(() => { + const control = new AbortController(); + + const fetchData = async () => { + try { + await getManifests({ + id: parseInt(manifest_id), + signal: control.signal, + }, token); + await getSUMSVersions(token); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + } + fetchData(); + return () => { + control.abort(); } - }; - - const changeVersion = (e) => { - setSoftwareVersion(e.target.value); - } - - useEffect(() => { - getData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [token]); + }, [manifest_id, token]); useEffect(() => { - const title = `Deploy ${manifestName} ${version}`; - setTitle(title); - setSitePath([ - { - label: "Deployments", - link: "/packages", - }, - { - label: title, - }, - ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [manifestName, version]); - - useEffect(() => { - if (!manifests || manifests.length === 0) return; - const data = manifests[0]; - - setManifestName(data.name); - setVersion(data.version); - setSUMSersion(data.sums || ""); - setCreateDate(LocalDateTimeString(data.created)); + if (manifests && manifests.length !== 0) { + setManifest(manifests[0]); + } }, [manifests]); + useEffect(() => { + if (manifest) { + const title = `Deploy ${manifest.name} ${manifest.version}`; + setTitle(title); + setSitePath([ + { + label: "Deployments", + link: "/packages", + }, + { + label: title, + }, + ]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manifest]); + if (redirect.length > 0) { return ; } + if (!manifest && !manifest?.name) { + return ( +
Loading...
+ ); + } + return (
-
- Created {createDate}. - - + + Created {LocalDateTimeString(manifest.created)}. + +
{`${selected.length} Selected`}
- + + { />} label="Car(default) or Fleet" /> - - + + + + + + + } + label="Only online" + /> + + + + } + label="Only online HMI" + /> + + + - - - - } - label="Only online" - /> - - - - } - label="Only online HMI" - /> - - - - - {sumsVersion.length === 0 && - - } - + + + {updateType === CAR_UPDATE ? diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx index 58d6c05..86b55c4 100644 --- a/src/components/useStyles.jsx +++ b/src/components/useStyles.jsx @@ -348,10 +348,18 @@ const useStyles = makeStyles((theme) => ({ formGridItem: { flexGrow: 1, }, + marginTop: { + marginTop: theme.spacing(2), + }, marginX: { marginTop: theme.spacing(2), marginBottom: theme.spacing(2), }, + closeModal: { + position: "absolute", + top: "5px", + right: "5px", + }, })); export default useStyles;