CEC-3672 Update manifest version on deploy (#277)

* CEC-3672 Add versions to CarUpdatesContext
Stub out getSoftwareVersions and updateManifestVersion

* CEC-3672 update version on deploy

* Validate version before updating
This commit is contained in:
John Wu
2023-02-09 11:51:23 -08:00
committed by GitHub
parent f863f37a9a
commit 9cf84fc426
10 changed files with 240 additions and 75 deletions

View File

@@ -3229,6 +3229,22 @@ exports[`App Route /package-deploy authenticated 1`] = `
<main <main
class="MuiContainer-root MuiContainer-maxWidthLg" class="MuiContainer-root MuiContainer-maxWidthLg"
> >
<div
class="MuiSnackbar-root MuiSnackbar-anchorOriginTopCenter"
>
<div
class="MuiPaper-root MuiSnackbarContent-root MuiPaper-elevation6"
direction="down"
role="alert"
style="opacity: 1; transform: scale(1, 1); transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,transform 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;"
>
<div
class="MuiSnackbarContent-message"
>
getSoftwareVersions is not a function
</div>
</div>
</div>
<div <div
data-testid="mocked-manifestsprovider" data-testid="mocked-manifestsprovider"
> >
@@ -3359,10 +3375,52 @@ exports[`App Route /package-deploy authenticated 1`] = `
</div> </div>
</div> </div>
<div <div
class="MuiGrid-root makeStyles-textRightAlign-0 MuiGrid-item MuiGrid-grid-md-4" class="MuiGrid-root MuiGrid-container MuiGrid-item MuiGrid-justify-content-xs-flex-end MuiGrid-grid-md-4"
> >
<div
class="MuiFormControl-root MuiFormControl-marginNormal"
>
<label
class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
data-shrink="false"
>
Software Version
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl"
>
<select
aria-invalid="false"
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
style="padding-left: 8px;"
>
<legend
class="PrivateNotchedOutline-legend-0"
style="width: 0.01px;"
>
<span>
</span>
</legend>
</fieldset>
</div>
</div>
<button <button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-formControl-0 makeStyles-textField-0 MuiButton-containedPrimary Mui-disabled Mui-disabled" class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary Mui-disabled Mui-disabled"
disabled="" disabled=""
tabindex="-1" tabindex="-1"
type="submit" type="submit"
@@ -3370,7 +3428,16 @@ exports[`App Route /package-deploy authenticated 1`] = `
<span <span
class="MuiButton-label" class="MuiButton-label"
> >
Deploy <svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"
/>
</svg>
</span> </span>
</button> </button>
</div> </div>

View File

@@ -1,10 +1,13 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import api from "../../services/updatesAPI"; import api from "../../services/updatesAPI";
import { validateSoftwareVersion } from "../../utils/softwareVersions";
import { validateStatusMessage } from "../../utils/statusMessage"; import { validateStatusMessage } from "../../utils/statusMessage";
export const SELECT_VERSION = "Select Version";
const FINAL_UPDATE_STATES = ["package_install_complete"]; const FINAL_UPDATE_STATES = ["package_install_complete"];
const CarUpdatesContext = React.createContext(); const CarUpdatesContext = React.createContext();
const SELECT_VERSION_OBJ = { version: SELECT_VERSION }
const validateDeployClosure = (data, propertyName, errPfx) => { const validateDeployClosure = (data, propertyName, errPfx) => {
if (data === null) { if (data === null) {
@@ -32,6 +35,7 @@ const validateDeployFleetUpdates = (data) => {
export const CarUpdatesProvider = ({ children }) => { export const CarUpdatesProvider = ({ children }) => {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [carUpdates, setCarUpdates] = useState([]); const [carUpdates, setCarUpdates] = useState([]);
const [versions, setVersions] = useState([SELECT_VERSION_OBJ]);
const [totalCarUpdates, setTotalCarUpdates] = useState(0); const [totalCarUpdates, setTotalCarUpdates] = useState(0);
const [delayCount, setDelayCount] = useState(0); const [delayCount, setDelayCount] = useState(0);
let progressTimer = 0; let progressTimer = 0;
@@ -238,20 +242,60 @@ export const CarUpdatesProvider = ({ children }) => {
return result; return result;
}; };
const getSoftwareVersions = async (token) => {
let result;
try {
setBusy(true);
result = await api.getSoftwareVersions(token);
if (result.error)
throw new Error(`Get software versions error. ${result.message}`);
result.data.unshift(SELECT_VERSION_OBJ)
setVersions(result.data);
} finally {
setBusy(false);
}
return result;
};
const updateManifestVersion = async (id, version, token) => {
let result;
try {
setBusy(true);
if (!validateSoftwareVersion(version)) throw new Error(`invalid version ${version}`);
result = await api.updateManifestVersion(id, version, token);
if (result.error)
throw new Error(`Update manifest version error. ${result.message}`);
} finally {
setBusy(false);
}
return result;
};
return ( return (
<CarUpdatesContext.Provider <CarUpdatesContext.Provider
value={{ value={{
busy, busy,
carUpdates, carUpdates,
totalCarUpdates, totalCarUpdates,
versions,
cancelUpdate, cancelUpdate,
deployCarUpdates, deployCarUpdates,
deployFleetUpdates, deployFleetUpdates,
getCarUpdates, getCarUpdates,
getLog, getLog,
getSoftwareVersions,
getVINUpdates, getVINUpdates,
startMonitor, startMonitor,
stopMonitor, stopMonitor,
updateManifestVersion,
}} }}
> >
{children} {children}

View File

@@ -0,0 +1,25 @@
import { FormControl, InputLabel, Select } from "@material-ui/core";
export const DropDownList = ({data, label, value, labelField, valueField, onChange, classes, ...others}) => {
return (
<FormControl
variant="outlined"
margin="normal"
>
<InputLabel className={classes.whiteBackground}>
{label}
</InputLabel>
<Select
native
value={value}
variant="outlined"
onChange={onChange}
{...others}
>
{data && data.map((item, index) => (
<option key={index} value={item[valueField || "value"]}>{item[labelField || "label"]}</option>
))}
</Select>
</FormControl>
);
}

View File

@@ -1,18 +1,19 @@
import { Button, FormControlLabel, Grid, Switch, Typography } from "@material-ui/core"; import { Button, FormControlLabel, Grid, Switch, Typography } from "@material-ui/core";
import clsx from "clsx"; import SendIcon from "@material-ui/icons/Send";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Redirect, useParams } from "react-router"; import { Redirect, useParams } from "react-router";
import { logger } from "../../../services/monitoring"; import { logger } from "../../../services/monitoring";
import { LocalDateTimeString } from "../../../utils/dates"; import { LocalDateTimeString } from "../../../utils/dates";
import { Permissions } from "../../../utils/roles"; import { Permissions } from "../../../utils/roles";
import { CarUpdatesProvider, useCarUpdatesContext } from "../../Contexts/CarUpdatesContext"; import { CarUpdatesProvider, SELECT_VERSION, useCarUpdatesContext } from "../../Contexts/CarUpdatesContext";
import { FleetProvider } from "../../Contexts/FleetContext"; import { FleetProvider } from "../../Contexts/FleetContext";
import { ManifestsProvider, useManifestsContext } from "../../Contexts/ManifestsContext"; import { ManifestsProvider, useManifestsContext } from "../../Contexts/ManifestsContext";
import { useStatusContext } from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext"; import { useUserContext } from "../../Contexts/UserContext";
import { VehicleProvider } from "../../Contexts/VehicleContext"; import { VehicleProvider } from "../../Contexts/VehicleContext";
import CarSelectionTable from "../../Controls/CarSelectionTable"; import CarSelectionTable from "../../Controls/CarSelectionTable";
import { DropDownList } from "../../Controls/DropDownList";
import FleetSelectionTable from "../../Controls/FleetSelectionTable"; import FleetSelectionTable from "../../Controls/FleetSelectionTable";
import { RoleWrap } from "../../Controls/RoleWrap"; import { RoleWrap } from "../../Controls/RoleWrap";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
@@ -25,7 +26,7 @@ const MainForm = () => {
const [updateType, setUpdateType] = useState(CAR_UPDATE); const [updateType, setUpdateType] = useState(CAR_UPDATE);
const {manifest_id} = useParams(); const {manifest_id} = useParams();
const {getManifests, manifests, busy} = useManifestsContext(); const {getManifests, manifests, busy} = useManifestsContext();
const {deployCarUpdates, deployFleetUpdates} = useCarUpdatesContext(); const {deployCarUpdates, deployFleetUpdates, getSoftwareVersions, versions, updateManifestVersion} = useCarUpdatesContext();
const { const {
groups, groups,
providers, providers,
@@ -39,6 +40,7 @@ const MainForm = () => {
const [createDate, setCreateDate] = useState(""); const [createDate, setCreateDate] = useState("");
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [softwareVersion, setSoftwareVersion] = useState(SELECT_VERSION);
const [redirect, setRedirect] = useState(""); const [redirect, setRedirect] = useState("");
const classes = useStyles(); const classes = useStyles();
@@ -77,6 +79,8 @@ const MainForm = () => {
const data = { const data = {
manifest_id: parseInt(manifest_id), manifest_id: parseInt(manifest_id),
} }
await updateManifestVersion(manifest_id, softwareVersion, token);
if (updateType === CAR_UPDATE) { if (updateType === CAR_UPDATE) {
data.vins = selected; data.vins = selected;
await deployCarUpdates(data, token); await deployCarUpdates(data, token);
@@ -96,13 +100,18 @@ const MainForm = () => {
const getData = async () => { const getData = async () => {
try { try {
getManifests({id: parseInt(manifest_id)}, token); await getManifests({id: parseInt(manifest_id)}, token);
await getSoftwareVersions(token);
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
logger.warn(e.stack); logger.warn(e.stack);
} }
}; };
const changeVersion = (e) => {
setSoftwareVersion(e.target.value);
}
useEffect(() => { useEffect(() => {
getData(); getData();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -162,16 +171,22 @@ const MainForm = () => {
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch}/> <SearchField classes={classes} onSearch={handleSearch}/>
</Grid> </Grid>
<Grid item md={4} className={classes.textRightAlign}> <Grid item md={4} container justifyContent="flex-end">
<DropDownList
label="Software Version"
labelField="version"
valueField="version"
value={softwareVersion}
data={versions}
classes={classes}
onChange={changeVersion} />
<Button <Button
type="submit" type="submit"
disabled={busy || selected.length === 0} disabled={busy || selected.length === 0 || softwareVersion === SELECT_VERSION}
variant="contained"
color="primary" color="primary"
className={clsx(classes.formControl, classes.textField)}
onClick={onSubmit} onClick={onSubmit}
> >
{busy ? "Deploying..." : "Deploy"} <SendIcon />
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -113,12 +113,11 @@ exports[`Manifest Details Component Render 1`] = `
</div> </div>
</div> </div>
<div <div
class="MuiFormControl-root makeStyles-form-0 MuiFormControl-marginNormal MuiFormControl-fullWidth" class="MuiFormControl-root MuiFormControl-marginNormal"
> >
<label <label
class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined" class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
data-shrink="false" data-shrink="false"
for="manifest-type"
> >
Type Type
</label> </label>
@@ -128,8 +127,6 @@ exports[`Manifest Details Component Render 1`] = `
<select <select
aria-invalid="false" aria-invalid="false"
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input" class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
id="send-manifest-type"
name="manifest-type"
> >
<option <option
value="standard" value="standard"
@@ -169,14 +166,13 @@ exports[`Manifest Details Component Render 1`] = `
</div> </div>
</div> </div>
<div <div
class="MuiFormControl-root makeStyles-form-0 MuiFormControl-marginNormal MuiFormControl-fullWidth" class="MuiFormControl-root MuiFormControl-marginNormal"
> >
<label <label
class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled" class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled"
data-shrink="true" data-shrink="true"
for="manifest-active"
> >
Type Active
</label> </label>
<div <div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl" class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl"
@@ -184,18 +180,16 @@ exports[`Manifest Details Component Render 1`] = `
<select <select
aria-invalid="false" aria-invalid="false"
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input" class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
id="send-manifest-active"
name="manifest-active"
> >
<option <option
value="true" value="true"
> >
active Active
</option> </option>
<option <option
value="false" value="false"
> >
archived Archived
</option> </option>
</select> </select>
<svg <svg

View File

@@ -1,17 +1,23 @@
import { Button, FormControl, TextField } from "@material-ui/core";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { Redirect } from "react-router"; import { Redirect } from "react-router";
import useStyles from "../../useStyles"; import { useParams } from "react-router-dom";
import { ManifestsProvider, useManifestsContext } from "../../Contexts/ManifestsContext"; import { ManifestsProvider, useManifestsContext } from "../../Contexts/ManifestsContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import { Button, FormControl, InputLabel, Select, TextField } from "@material-ui/core"; import { useUserContext } from "../../Contexts/UserContext";
import { DropDownList } from "../../Controls/DropDownList";
import useStyles from "../../useStyles";
const manifestTypes = [ const manifestTypes = [
{ value: "standard", label: "Standard" }, { value: "standard", label: "Standard" },
{ value: "forced", label: "Forced" }, { value: "forced", label: "Forced" },
]; ];
const activeStates = [
{value: true, label: "Active" },
{value: false, label: "Archived" },
];
const emptyManifest = { const emptyManifest = {
name: "", name: "",
version: "", version: "",
@@ -138,53 +144,8 @@ const MainForm = () => {
fullWidth fullWidth
onChange={changeName} onChange={changeName}
/> />
<FormControl <DropDownList label="Type" data={manifestTypes} classes={classes} onChange={changeType} value={type} />
className={classes.form} <DropDownList label="Active" data={activeStates} classes={classes} onChange={changeActive} value={active}/>
variant="outlined"
fullWidth
margin="normal"
>
<InputLabel htmlFor="manifest-type" className={classes.whiteBackground}>
Type
</InputLabel>
<Select
native
value={type}
variant="outlined"
inputProps={{
name: "manifest-type",
id: "send-manifest-type",
}}
onChange={changeType}
>
{manifestTypes.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</Select>
</FormControl>
<FormControl
className={classes.form}
variant="outlined"
fullWidth
margin="normal"
>
<InputLabel htmlFor="manifest-active" className={classes.whiteBackground}>
Type
</InputLabel>
<Select
native
value={active}
variant="outlined"
inputProps={{
name: "manifest-active",
id: "send-manifest-active",
}}
onChange={changeActive}
>
<option key={0} value={true}>active</option>
<option key={1} value={false}>archived</option>
</Select>
</FormControl>
<Button <Button
type="submit" type="submit"
aria-label="send command" aria-label="send command"

View File

@@ -26,6 +26,16 @@ const updatesAPI = {
cancelCarUpdate: async (id, token) => { cancelCarUpdate: async (id, token) => {
return { message: "OK" }; return { message: "OK" };
}, },
getSoftwareVersions: async (token) => {
return {
"data": ["2023.02.01.0.0.A", "2023.02.01.0.0.B"]
};
},
updateManifestVersion: async (_id, version) => {
return { version };
},
}; };
export default updatesAPI; export default updatesAPI;

View File

@@ -1,5 +1,5 @@
import { import {
addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions
} from "../utils/http"; } from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
@@ -86,6 +86,31 @@ const updatesAPI = {
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler); .catch(errorHandler);
}, },
getSoftwareVersions: async (token) => {
return fetch(`${API_ENDPOINT}/manifest/versions`, {
method: "GET",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
})
.then(fetchRespHandler)
.catch(errorHandler);
},
updateManifestVersion: async (id, version, token) => {
return fetch(`${API_ENDPOINT}/manifests/${id}/version`, {
method: "PUT",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token),
),
body: JSON.stringify({ version }),
})
.then(fetchRespHandler)
.catch(errorHandler);
}
}; };
export default updatesAPI; export default updatesAPI;

View File

@@ -0,0 +1,5 @@
const rxSoftwareVersion = /^\d{4}\.(0[1-9]|1[0-2])\.\d{2}\.\d{2}(\.[\d\w]{1})?$/i;
export const validateSoftwareVersion = (version) => {
return rxSoftwareVersion.test(version);
}

View File

@@ -0,0 +1,19 @@
import { validateSoftwareVersion } from "./softwareVersions";
describe("Software versions", () => {
it("validation", () =>{
expect(validateSoftwareVersion("2023.12.01.01.A")).toEqual(true);
expect(validateSoftwareVersion("2023.10.01.01.A")).toEqual(true);
expect(validateSoftwareVersion("2023.09.01.01.A")).toEqual(true);
expect(validateSoftwareVersion("2023.13.01.01.A")).toEqual(false);
expect(validateSoftwareVersion("2023.12.01.01")).toEqual(true);
expect(validateSoftwareVersion("2023.10.01.01")).toEqual(true);
expect(validateSoftwareVersion("2023.09.01.01")).toEqual(true);
expect(validateSoftwareVersion("2023.13.01.01")).toEqual(false);
expect(validateSoftwareVersion("2023.12.01")).toEqual(false);
expect(validateSoftwareVersion("2023.10.AA.01")).toEqual(false);
expect(validateSoftwareVersion("2023.09.01.AA")).toEqual(false);
expect(validateSoftwareVersion("202A.09.01.01")).toEqual(false);
expect(validateSoftwareVersion("2023.1A.01.01")).toEqual(false);
})
});