Merge branch 'main' into CEC-5443

This commit is contained in:
Paul Adamsen
2024-01-08 15:05:22 -05:00
committed by GitHub
25 changed files with 670 additions and 174 deletions

View File

@@ -3515,7 +3515,7 @@ exports[`App Route /issues authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending" aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
@@ -3529,11 +3529,11 @@ exports[`App Route /issues authenticated 1`] = `
<span <span
class="makeStyles-hiddenSortSpan-0" class="makeStyles-hiddenSortSpan-0"
> >
sorted ascending sorted descending
</span> </span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -4578,22 +4578,16 @@ exports[`App Route /package-deploy authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active" class="MuiButtonBase-root MuiTableSortLabel-root"
role="button" role="button"
tabindex="0" tabindex="0"
> >
VIN VIN
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted ascending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
@@ -4699,19 +4693,25 @@ exports[`App Route /package-deploy authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root" class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button" role="button"
tabindex="0" tabindex="0"
> >
Updated Updated
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted descending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -5538,7 +5538,7 @@ exports[`App Route /package-status authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending" aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
@@ -5552,11 +5552,11 @@ exports[`App Route /package-status authenticated 1`] = `
<span <span
class="makeStyles-hiddenSortSpan-0" class="makeStyles-hiddenSortSpan-0"
> >
sorted ascending sorted descending
</span> </span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -6744,7 +6744,7 @@ exports[`App Route /packages authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending" aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
@@ -6758,11 +6758,11 @@ exports[`App Route /packages authenticated 1`] = `
<span <span
class="makeStyles-hiddenSortSpan-0" class="makeStyles-hiddenSortSpan-0"
> >
sorted ascending sorted descending
</span> </span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -12964,22 +12964,16 @@ exports[`App Route /vehicles authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active" class="MuiButtonBase-root MuiTableSortLabel-root"
role="button" role="button"
tabindex="0" tabindex="0"
> >
VIN VIN
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted ascending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
@@ -13085,19 +13079,25 @@ exports[`App Route /vehicles authenticated 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root" class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button" role="button"
tabindex="0" tabindex="0"
> >
Updated Updated
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted descending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >

View File

@@ -14,7 +14,7 @@ export default forwardRef(({
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
async submit() { async submit() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(5, ids.length); const taskRunner = new TaskRunner(30, ids.length);
let errorCount = 0; let errorCount = 0;
const task = (id, index) => { const task = (id, index) => {

View File

@@ -18,7 +18,7 @@ const commands = [
async function getECUsByVINs(vins, token) { async function getECUsByVINs(vins, token) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(10, vins.length); const taskRunner = new TaskRunner(30, vins.length);
const task = (vin) => { const task = (vin) => {
return async () => api.getECUs({ vin, unique: true }, token) return async () => api.getECUs({ vin, unique: true }, token)

View File

@@ -14,7 +14,7 @@ export default forwardRef(({
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
async submit() { async submit() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(5, ids.length); const taskRunner = new TaskRunner(30, ids.length);
let errorCount = 0; let errorCount = 0;
const task = (id, index) => { const task = (id, index) => {

View File

@@ -23,7 +23,7 @@ export default forwardRef(({
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
async submit() { async submit() {
return new Promise((resolve) => { return new Promise((resolve) => {
const taskRunner = new TaskRunner(5, ids.length); const taskRunner = new TaskRunner(30, ids.length);
ids.forEach((vin) => { ids.forEach((vin) => {
taskRunner.push(async () => { taskRunner.push(async () => {

View File

@@ -20,7 +20,7 @@ export default forwardRef(({
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
async submit() { async submit() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(5, ids.length); const taskRunner = new TaskRunner(30, ids.length);
let errorCount = 0; let errorCount = 0;
const task = (vin, index) => { const task = (vin, index) => {

View File

@@ -173,22 +173,16 @@ exports[`VehicleTable Render 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active" class="MuiButtonBase-root MuiTableSortLabel-root"
role="button" role="button"
tabindex="0" tabindex="0"
> >
VIN VIN
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted ascending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
@@ -294,19 +288,25 @@ exports[`VehicleTable Render 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root" class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button" role="button"
tabindex="0" tabindex="0"
> >
Updated Updated
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted descending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >

View File

@@ -63,8 +63,8 @@ const CarSelectionTable = (props) => {
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("vin"); const [orderBy, setOrderBy] = useState("updated_at");
const [order, setOrder] = useState("asc"); const [order, setOrder] = useState("desc");
const { getVehicles, vehicles, totalVehicles } = useVehicleContext(); const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
const { setMessage } = useStatusContext(); const { setMessage } = useStatusContext();

View File

@@ -1,10 +1,11 @@
import { FormControl, InputLabel, Select } from "@material-ui/core"; 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 ( return (
<FormControl <FormControl
variant="outlined" variant="outlined"
margin="normal" margin="normal"
fullWidth={fullWidth}
> >
<InputLabel className={classes.whiteBackground}> <InputLabel className={classes.whiteBackground}>
{label} {label}

View File

@@ -64,7 +64,7 @@ const IssueSelectionTable = (props) => {
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("created_at"); const [orderBy, setOrderBy] = useState("created_at");
const [order, setOrder] = useState("asc"); const [order, setOrder] = useState("desc");
const { deleteIssue, getIssues, issues, totalIssues = 0 } = useIssueContext(); const { deleteIssue, getIssues, issues, totalIssues = 0 } = useIssueContext();
const { groups, providers } = useUserContext(); const { groups, providers } = useUserContext();
const { setMessage } = useStatusContext(); const { setMessage } = useStatusContext();

View File

@@ -0,0 +1,44 @@
import {
Box,
FormControl,
FormHelperText,
Input,
InputLabel,
} from "@material-ui/core";
const PREFIX = "number-field";
function kebab(str) {
return str.replaceAll(" ", "-").toLowerCase();
}
export function NumberField({
name,
description,
value = 0,
setValue = () => { },
...rest
}) {
const inputId = `${PREFIX}-${kebab(name)}`;
const describeId = `${PREFIX}-${kebab(name)}-describe`;
const handleChange = (event) => {
setValue(event.target.value);
}
return (
<Box sx={{ my: 2 }}>
<FormControl fullWidth>
<InputLabel htmlFor={inputId}>{name}</InputLabel>
<Input
id={inputId}
aria-describedby={describeId}
type="number"
value={value}
onChange={handleChange}
{...rest}
/>
<FormHelperText id={describeId}>{description}</FormHelperText>
</FormControl>
</Box>
);
}

View File

@@ -0,0 +1,55 @@
import React from "react";
import { fireEvent, render } from "@testing-library/react";
import { NumberField } from "./index";
describe("NumberField", () => {
it("renders with form label and aria describe", () => {
const { getByText } = render(
<NumberField
name="My First Field"
description="Some helper text"
/>
);
const labelEl = getByText("My First Field");
const describeEl = getByText("Some helper text");
expect(labelEl.htmlFor).toEqual("number-field-my-first-field");
expect(describeEl.id).toEqual("number-field-my-first-field-describe");
});
it("input is of type number", () => {
const { getByLabelText } = render(
<NumberField
name="My First Field"
description="Some helper text"
/>
);
const inputEl = getByLabelText("My First Field", { selector: "input" });
expect(inputEl.type).toEqual("number");
});
it("updates parent state", () => {
let mockState = 0;
const mockSetState = jest.fn((value) => mockState = value);
const { getByLabelText } = render(
<NumberField
name="My First Field"
description="Enter a number"
value={mockState}
setValue={mockSetState}
/>
);
const inputEl = getByLabelText("My First Field", { selector: "input" });
fireEvent.change(inputEl, { target: { value: "1" } });
expect(mockState).toEqual("1");
expect(mockSetState.mock.calls.length).toEqual(1);
});
})

View File

@@ -0,0 +1 @@
export * from "./NumberField";

View File

@@ -18,6 +18,7 @@ const commands = [
{ displayname: "Set CAN Network State", val: "can_network" }, { displayname: "Set CAN Network State", val: "can_network" },
{ displayname: "Set Remote Ignition", val: "remote_ignition" }, { displayname: "Set Remote Ignition", val: "remote_ignition" },
{ displayname: "Send Wake Up SMS", val: "sms" }, { displayname: "Send Wake Up SMS", val: "sms" },
{ displayname: "Update SecOC keys", val: "write_secoc_key" },
{ displayname: "Read ECU versions", val: "read_ecu_versions" }, { displayname: "Read ECU versions", val: "read_ecu_versions" },
] ]
@@ -106,6 +107,8 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => {
} }
} else if (currentCommand === "read_ecu_versions") { } else if (currentCommand === "read_ecu_versions") {
await sendDiagnosticCommand([vin], { command: currentCommand, ecu_name: currentECU }, token); await sendDiagnosticCommand([vin], { command: currentCommand, ecu_name: currentECU }, token);
} else if (currentCommand === "write_secoc_key") {
await sendDiagnosticCommand([vin], { command: currentCommand }, token);
} }
setMessage(`Sent diagnostic command to ${vin}`); setMessage(`Sent diagnostic command to ${vin}`);
} catch (error) { } catch (error) {

View File

@@ -119,22 +119,16 @@ exports[`FleetVehicleAdd Render 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="ascending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active" class="MuiButtonBase-root MuiTableSortLabel-root"
role="button" role="button"
tabindex="0" tabindex="0"
> >
VIN VIN
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted ascending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc"
@@ -240,19 +234,25 @@ exports[`FleetVehicleAdd Render 1`] = `
</span> </span>
</th> </th>
<th <th
aria-sort="descending"
class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter" class="MuiTableCell-root MuiTableCell-head MuiTableCell-alignCenter"
scope="col" scope="col"
> >
<span <span
aria-disabled="false" aria-disabled="false"
class="MuiButtonBase-root MuiTableSortLabel-root" class="MuiButtonBase-root MuiTableSortLabel-root MuiTableSortLabel-active"
role="button" role="button"
tabindex="0" tabindex="0"
> >
Updated Updated
<span
class="makeStyles-hiddenSortSpan-0"
>
sorted descending
</span>
<svg <svg
aria-hidden="true" aria-hidden="true"
class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionAsc" class="MuiSvgIcon-root MuiTableSortLabel-icon MuiTableSortLabel-iconDirectionDesc"
focusable="false" focusable="false"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >

View File

@@ -12,7 +12,7 @@ import {
} from "@material-ui/core"; } from "@material-ui/core";
import AddCircleIcon from "@material-ui/icons/AddCircle"; import AddCircleIcon from "@material-ui/icons/AddCircle";
import clsx from "clsx"; import clsx from "clsx";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logger } from "../../../../../services/monitoring"; import { logger } from "../../../../../services/monitoring";
@@ -70,6 +70,7 @@ const MainForm = ({ name }) => {
const [order, setOrder] = useState("desc"); const [order, setOrder] = useState("desc");
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const componentMounted = useRef(true);
const classes = useStyles(); const classes = useStyles();
const { setMessage } = useStatusContext(); const { setMessage } = useStatusContext();
const { const {
@@ -103,7 +104,9 @@ const MainForm = ({ name }) => {
}, },
token token
); );
if (componentMounted.current) {
watchFleetVehicles.start({ token }); watchFleetVehicles.start({ token });
}
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
logger.warn(e.stack); logger.warn(e.stack);
@@ -111,6 +114,7 @@ const MainForm = ({ name }) => {
} }
})(); })();
return () => { return () => {
componentMounted.current = false;
watchFleetVehicles.end(); watchFleetVehicles.end();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -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 (
<>
<IconButton onClick={handleOpen} disabled={disabled}>
<SendIcon />
</IconButton>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<IconButton
color="action"
onClick={handleClose}
edge={false}
className={classes.closeModal}
>
<CloseIcon />
</IconButton>
<Typography id="modal-modal-title" variant="h6" component="h2">
Complete Manifest
</Typography>
<Typography id="modal-modal-description">
{manifest.name ? (<i>{manifest.name}</i>) : `This manifest`} is incomplete, and cannot be deployed without all required fields filled out.
</Typography>
<br />
<DropDownList
label="Sums Version"
labelField="version"
valueField="version"
value={localManifest.sums}
data={versions}
classes={classes}
onChange={(e) => handleManifestField("sums", e.target.value)}
error={!satisfiesRequiredFields[0]}
fullWidth
/>
<ConfigureInput
name="Max Attempts"
description="How many times should the car try and install the mainfest?"
value={localManifest.max_attempts}
setValue={(value) => handleManifestField("max_attempts", value)}
error={!satisfiesRequiredFields[1]}
/>
<ConfigureInput
name="Update Duration"
description="How long should the car try installing the manifest for?"
value={localManifest.update_duration}
setValue={(value) => handleManifestField("update_duration", value)}
error={!satisfiesRequiredFields[2]}
/>
<TextField
label="Release Notes"
value={localManifest.release_notes}
onChange={(value) => handleManifestField("release_notes", value)}
fullWidth
/>
<Button
variant="contained"
className={classes.marginTop}
disabled={needsConfiguration}
onClick={onSubmit}
>
Update and Send
</Button>
</Box>
</Modal>
</>
);
}
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 (
<Box sx={{ my: 2 }}>
<FormControl fullWidth>
<InputLabel htmlFor={inputId}>{name}</InputLabel>
<Input
id={inputId}
aria-describedby={descriptionId}
type="number"
value={value}
onChange={handleChange}
error={error}
/>
<FormHelperText id={descriptionId}>{description}</FormHelperText>
</FormControl>
</Box>
);
}

View File

@@ -1,13 +1,11 @@
import { Button, Checkbox, FormControlLabel, Grid, MenuItem, Switch, Typography } from "@material-ui/core"; import { Checkbox, FormControlLabel, Grid, MenuItem, Switch, Typography, Box } 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, SELECT_VERSION, useCarUpdatesContext } from "../../Contexts/CarUpdatesContext"; import { CarUpdatesProvider, 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";
@@ -15,11 +13,11 @@ 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 OptionsDropdown from "../../Controls/OptionsDropdown"; import OptionsDropdown from "../../Controls/OptionsDropdown";
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";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import Configure from "./Configure";
const CAR_UPDATE = false; const CAR_UPDATE = false;
const FLEET_UPDATE = true; const FLEET_UPDATE = true;
@@ -28,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, getSUMSVersions, versions, updateSUMSVersion } = useCarUpdatesContext(); const { deployCarUpdates, deployFleetUpdates, getSUMSVersions, versions } = useCarUpdatesContext();
const { const {
groups, groups,
providers, providers,
@@ -37,15 +35,11 @@ const MainForm = () => {
}, },
} = useUserContext(); } = useUserContext();
const { setMessage, setTitle, setSitePath } = useStatusContext(); const { setMessage, setTitle, setSitePath } = useStatusContext();
const [manifestName, setManifestName] = useState(""); const [manifest, setManifest] = useState({});
const [version, setVersion] = useState("");
const [sumsVersion, setSUMSersion] = useState("");
const [createDate, setCreateDate] = useState("");
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [online, setOnline] = useState(false); const [online, setOnline] = useState(false);
const [onlineHMI, setOnlineHMI] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false);
const [softwareVersion, setSoftwareVersion] = useState(SELECT_VERSION);
const [redirect, setRedirect] = useState(""); const [redirect, setRedirect] = useState("");
const classes = useStyles(); const classes = useStyles();
@@ -86,15 +80,11 @@ const MainForm = () => {
} }
}; };
const onSubmit = async (event) => { const onSubmit = async () => {
try { try {
event.preventDefault();
const data = { const data = {
manifest_id: parseInt(manifest_id), manifest_id: parseInt(manifest_id),
} }
if (sumsVersion.length === 0) {
await updateSUMSVersion(manifest_id, softwareVersion, token);
}
if (updateType === CAR_UPDATE) { if (updateType === CAR_UPDATE) {
data.vins = selected; data.vins = selected;
@@ -104,7 +94,7 @@ const MainForm = () => {
await deployFleetUpdates(data, token); await deployFleetUpdates(data, token);
} }
setMessage( setMessage(
`Deployed ${manifestName} ${version} to ${selected.length} cars` `Deployed ${manifest.name} ${manifest.version} to ${selected.length} cars`
); );
setRedirect(`/package-status/${manifest_id}`); setRedirect(`/package-status/${manifest_id}`);
} catch (e) { } catch (e) {
@@ -113,27 +103,37 @@ const MainForm = () => {
} }
}; };
const getData = async () => { useEffect(() => {
const control = new AbortController();
const fetchData = async () => {
try { try {
await getManifests({ id: parseInt(manifest_id) }, token); await getManifests({
id: parseInt(manifest_id),
signal: control.signal,
}, token);
await getSUMSVersions(token); await getSUMSVersions(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);
} }
fetchData();
useEffect(() => { return () => {
getData(); control.abort();
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]); }, [manifest_id, token]);
useEffect(() => { useEffect(() => {
const title = `Deploy ${manifestName} ${version}`; if (manifests && manifests.length !== 0) {
setManifest(manifests[0]);
}
}, [manifests]);
useEffect(() => {
if (manifest) {
const title = `Deploy ${manifest.name} ${manifest.version}`;
setTitle(title); setTitle(title);
setSitePath([ setSitePath([
{ {
@@ -144,34 +144,36 @@ const MainForm = () => {
label: title, label: title,
}, },
]); ]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [manifestName, version]); }, [manifest]);
useEffect(() => {
if (!manifests || manifests.length === 0) return;
const data = manifests[0];
setManifestName(data.name);
setVersion(data.version);
setSUMSersion(data.sums || "");
setCreateDate(LocalDateTimeString(data.created));
}, [manifests]);
if (redirect.length > 0) { if (redirect.length > 0) {
return <Redirect to={redirect} />; return <Redirect to={redirect} />;
} }
if (!manifest && !manifest?.name) {
return (
<div>Loading...</div>
);
}
return ( return (
<div className={classes.paper}> <div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}"> <form className={classes.form} noValidate>
<Typography variant="body2">Created {createDate}.</Typography> <Typography variant="body2">Created {LocalDateTimeString(manifest.created)}.</Typography>
<Grid container className={classes.root} spacing={2}> <Grid container
<Grid item md={2}> className={classes.root}
spacing={2}
columns={{ xs: 4, sm: 6, md: 12 }}
>
<Grid item xs={2}>
<div <div
className={classes.labelInline} className={classes.labelInline}
>{`${selected.length} Selected`}</div> >{`${selected.length} Selected`}</div>
</Grid> </Grid>
<Grid item md={2} className={classes.textCenterAlign}>
<Grid item xs={2}>
<RoleWrap <RoleWrap
groups={groups} groups={groups}
providers={providers} providers={providers}
@@ -184,10 +186,10 @@ const MainForm = () => {
/>} label="Car(default) or Fleet" /> />} label="Car(default) or Fleet" />
</RoleWrap> </RoleWrap>
</Grid> </Grid>
<Grid item md={2} className={classes.textCenterAlign}>
<Grid item xs={4} sm={6}>
<Box sx={{ display: "flex", width: "100%" }}>
<SearchField classes={classes} onSearch={handleSearch} /> <SearchField classes={classes} onSearch={handleSearch} />
</Grid>
<Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}>
<OptionsDropdown listId="filter-menu"> <OptionsDropdown listId="filter-menu">
<MenuItem> <MenuItem>
<FormControlLabel <FormControlLabel
@@ -204,26 +206,18 @@ const MainForm = () => {
/> />
</MenuItem> </MenuItem>
</OptionsDropdown> </OptionsDropdown>
</Box>
</Grid> </Grid>
<Grid item md={4} container justifyContent="flex-end">
{sumsVersion.length === 0 && <Grid item xs="auto">
<DropDownList <Configure
label="Software Version" manifest={manifest}
labelField="version"
valueField="version"
value={softwareVersion}
data={versions}
classes={classes} classes={classes}
onChange={changeVersion} /> versions={versions}
} disabled={busy || selected.length === 0}
<Button submit={onSubmit}
type="submit" key={manifest.name} // to trigger re-render of child on prop change
disabled={busy || selected.length === 0 || (sumsVersion.length === 0 && softwareVersion === SELECT_VERSION)} />
color="primary"
onClick={onSubmit}
>
<SendIcon />
</Button>
</Grid> </Grid>
</Grid> </Grid>
{updateType === CAR_UPDATE ? {updateType === CAR_UPDATE ?

View File

@@ -112,7 +112,7 @@ const MainForm = () => {
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id"); const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("asc"); const [order, setOrder] = useState("desc");
const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", ""); const [search, setSearch] = useLocalStorage("DEPLOYMENT_SEARCH", "");
const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software"); const [active, setActive] = useLocalStorage("DEPLOYMENT_TAB_TOGGLE", "software");

View File

@@ -63,7 +63,7 @@ const MainForm = () => {
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [orderBy, setOrderBy] = useState("id"); const [orderBy, setOrderBy] = useState("id");
const [order, setOrder] = useState("asc"); const [order, setOrder] = useState("desc");
const [ids, setIds] = useState([]); const [ids, setIds] = useState([]);
const { getManifests, manifests } = useManifestsContext(); const { getManifests, manifests } = useManifestsContext();
const { const {

View File

@@ -112,6 +112,43 @@ exports[`Manifest Details Component Render 1`] = `
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled"
data-shrink="true"
for="release_notes"
id="release_notes-label"
>
Release Notes
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="release_notes"
maxlength="255"
name="release_notes"
type="text"
value="https://releasenotes.com"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-0 PrivateNotchedOutline-legendNotched-0"
>
<span>
Release Notes
</span>
</legend>
</fieldset>
</div>
</div>
<div <div
class="MuiFormControl-root MuiFormControl-marginNormal" class="MuiFormControl-root MuiFormControl-marginNormal"
> >
@@ -334,6 +371,68 @@ exports[`Manifest Details Component Render 1`] = `
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div
class="MuiBox-root MuiBox-root-0"
>
<div
class="MuiFormControl-root MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled"
data-shrink="true"
for="number-field-update-duration"
>
Update Duration
</label>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl"
>
<input
aria-describedby="number-field-update-duration-describe"
aria-invalid="false"
class="MuiInputBase-input MuiInput-input"
id="number-field-update-duration"
type="number"
value="0"
/>
</div>
<p
class="MuiFormHelperText-root MuiFormHelperText-filled"
id="number-field-update-duration-describe"
/>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-0"
>
<div
class="MuiFormControl-root MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled"
data-shrink="true"
for="number-field-max-attempts"
>
Max Attempts
</label>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl"
>
<input
aria-describedby="number-field-max-attempts-describe"
aria-invalid="false"
class="MuiInputBase-input MuiInput-input"
id="number-field-max-attempts"
type="number"
value="0"
/>
</div>
<p
class="MuiFormHelperText-root MuiFormHelperText-filled"
id="number-field-max-attempts-describe"
/>
</div>
</div>
<button <button
aria-label="send command" aria-label="send command"
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth" class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"

View File

@@ -10,6 +10,7 @@ import { DropDownList } from "../../Controls/DropDownList";
import { RoleWrap } from "../../Controls/RoleWrap"; import { RoleWrap } from "../../Controls/RoleWrap";
import { Permissions } from "../../../utils/roles"; import { Permissions } from "../../../utils/roles";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import { NumberField } from "../../Controls/NumberField";
const manifestTypes = [ const manifestTypes = [
{ value: "standard", label: "Standard" }, { value: "standard", label: "Standard" },
@@ -51,15 +52,22 @@ const MainForm = () => {
const { setMessage, setTitle, setSitePath } = useStatusContext(); const { setMessage, setTitle, setSitePath } = useStatusContext();
const [name, setName] = useState(""); const [name, setName] = useState("");
const [releaseNotes, setReleaseNotes] = useState("");
const [type, setType] = useState(""); const [type, setType] = useState("");
const [active, setActive] = useState(true); // So !active = archived const [active, setActive] = useState(true); // So !active = archived
const [rollback, setRollback] = useState(true); const [rollback, setRollback] = useState(true);
const [env, setEnv] = useState("current"); const [env, setEnv] = useState("current");
const [updateDuration, setUpdateDuration] = useState(0);
const [maxAttempts, setMaxAttempts] = useState(0);
const changeName = (e) => { const changeName = (e) => {
setName(e.target.value); setName(e.target.value);
} }
const changeReleaseNotes = (e) => {
setReleaseNotes(e.target.value);
}
const changeType = (e) => { const changeType = (e) => {
setType(e.target.value); setType(e.target.value);
}; };
@@ -76,10 +84,33 @@ const MainForm = () => {
setEnv(e.target.value) setEnv(e.target.value)
} }
const changeUpdateDuration = (value) => {
if (value < 0) {
return;
}
setUpdateDuration(value);
}
const changeMaxAttempts = (value) => {
if (value < 0) {
return;
}
setMaxAttempts(value);
}
const onSubmit = async (e) => { const onSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const result = await updateManifest(manifest_id, { name, type, active, rollback, env }, token); const result = await updateManifest(manifest_id, {
name,
release_notes: releaseNotes,
type,
active,
rollback,
env,
update_duration: parseInt(updateDuration),
max_attempts: parseInt(maxAttempts),
}, token);
if (!result || result.error) return; if (!result || result.error) return;
setMessage(`Updated manifest ${manifest_id}`); setMessage(`Updated manifest ${manifest_id}`);
@@ -110,10 +141,13 @@ const MainForm = () => {
} else { } else {
setManifest(result); setManifest(result);
setName(result.name); setName(result.name);
setReleaseNotes(result.release_notes);
setType(result.type); setType(result.type);
setActive(result.active); setActive(result.active);
setRollback(result.rollback); setRollback(result.rollback);
setEnv(result.env ?? "current"); setEnv(result.env ?? "current");
setUpdateDuration(result.update_duration);
setMaxAttempts(result.max_attempts);
} }
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
@@ -176,12 +210,35 @@ const MainForm = () => {
fullWidth fullWidth
onChange={changeName} onChange={changeName}
/> />
<TextField
id="release_notes"
name="release_notes"
label="Release Notes"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
value={releaseNotes}
fullWidth
onChange={changeReleaseNotes}
/>
<DropDownList label="Type" data={manifestTypes} classes={classes} onChange={changeType} value={type} /> <DropDownList label="Type" data={manifestTypes} classes={classes} onChange={changeType} value={type} />
<DropDownList label="Active" data={activeStates} classes={classes} onChange={changeActive} value={active} /> <DropDownList label="Active" data={activeStates} classes={classes} onChange={changeActive} value={active} />
<DropDownList label="Rollback" data={booleanStates} classes={classes} onChange={changeRollback} value={rollback} /> <DropDownList label="Rollback" data={booleanStates} classes={classes} onChange={changeRollback} value={rollback} />
{ {
ENVS.length > 1 && <DropDownList label="ECC Keys" data={ENVS} classes={classes} onChange={changeEnv} value={env} /> ENVS.length > 1 && <DropDownList label="ECC Keys" data={ENVS} classes={classes} onChange={changeEnv} value={env} />
} }
<NumberField
name="Update Duration"
value={updateDuration}
setValue={changeUpdateDuration}
/>
<NumberField
name="Max Attempts"
value={maxAttempts}
setValue={changeMaxAttempts}
/>
<Button <Button
type="submit" type="submit"
aria-label="send command" aria-label="send command"

View File

@@ -348,10 +348,18 @@ const useStyles = makeStyles((theme) => ({
formGridItem: { formGridItem: {
flexGrow: 1, flexGrow: 1,
}, },
marginTop: {
marginTop: theme.spacing(2),
},
marginX: { marginX: {
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
}, },
closeModal: {
position: "absolute",
top: "5px",
right: "5px",
},
})); }));
export default useStyles; export default useStyles;

View File

@@ -8,7 +8,7 @@ export const useUpdateManifest = (token) => {
const remove = async () => { const remove = async () => {
return new Promise((resolve) => { return new Promise((resolve) => {
const taskRunner = new TaskRunner(5, updateManifestIds.length); const taskRunner = new TaskRunner(30, updateManifestIds.length);
let errorCount = 0; let errorCount = 0;
const task = (id) => { const task = (id) => {