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 (