Merge branch 'development' into main

This commit is contained in:
jwu-fisker
2021-05-10 10:08:37 -07:00
17 changed files with 1967 additions and 1543 deletions

30
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${file}"
},
{
"name": "Debug CRA Tests",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
"args": ["test", "--runInBand", "--no-cache", "--watchAll=false"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": { "CI": "true" },
"disableOptimisticBPs": true
}
]
}

2251
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,19 @@
"version": "0.1.1", "version": "0.1.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@datadog/browser-rum": "^2.6.2", "@datadog/browser-rum": "^2.8.1",
"@material-ui/core": "^4.11.3", "@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.8", "@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.2", "@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.6.0", "@testing-library/user-event": "^12.8.3",
"axios": "^0.21.1", "axios": "^0.21.1",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"material-ui-dropzone": "^3.5.0", "material-ui-dropzone": "^3.5.0",
"react": "^17.0.1", "react": "^17.0.2",
"react-dom": "^17.0.1", "react-dom": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.3",
"web-vitals": "^0.2.4" "web-vitals": "^0.2.4"
}, },
"scripts": { "scripts": {
@@ -47,6 +47,6 @@
"node": "12.20.1" "node": "12.20.1"
}, },
"devDependencies": { "devDependencies": {
"react-test-renderer": "^17.0.1" "react-test-renderer": "^17.0.2"
} }
} }

View File

@@ -117,11 +117,6 @@ describe("App", () => {
await check("/update/1", "h1", "Edit Update Package 1"); await check("/update/1", "h1", "Edit Update Package 1");
}); });
it("Route /carupdate-deploy authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-deploy/1", "h1", "Deploy [1]");
});
it("Route /carupdate-status authenticated", async () => { it("Route /carupdate-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT); setToken(TEST_AUTH_OBJECT);
await check("/carupdate-status/1", "h1", ""); await check("/carupdate-status/1", "h1", "");
@@ -145,4 +140,9 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT); setToken(TEST_AUTH_OBJECT);
await check("/page-not-found", "h1", "Page Not Found"); await check("/page-not-found", "h1", "Page Not Found");
}); });
})
it("Route /carupdate-deploy authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-deploy/1", "h1", "Deploy [1]");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,20 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, Redirect } from "react-router"; import { useParams, Redirect } from "react-router";
import { import { Button, TextField, Typography } from "@material-ui/core";
Button,
Chip,
FormControl,
Input,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
useTheme,
} from "@material-ui/core";
import { import {
UpdatesProvider, UpdatesProvider,
useUpdatesContext, useUpdatesContext,
} from "../../Contexts/UpdatesContext"; } from "../../Contexts/UpdatesContext";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext"; import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import CarSelection from "../../Cars/CarSelection";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import { tsLocalDateTimeString } from "../../../utils/dates"; import { tsLocalDateTimeString } from "../../../utils/dates";
import menuItemStyle from "../../menuItemStyle";
const MainForm = () => { const MainForm = () => {
const { packageid } = useParams(); const { packageid } = useParams();
const { getPackages, createCarUpdates, packages, busy } = useUpdatesContext(); const { getPackages, createCarUpdates, packages, busy } = useUpdatesContext();
const { getVehicles, vehicles } = useVehicleContext();
const { const {
token: { token: {
idToken: { jwtToken: token }, idToken: { jwtToken: token },
@@ -46,10 +30,7 @@ const MainForm = () => {
const [selectedVehicles, setSelectedVehicles] = useState([]); const [selectedVehicles, setSelectedVehicles] = useState([]);
const [redirect, setRedirect] = useState(""); const [redirect, setRedirect] = useState("");
const classes = useStyles(); const classes = useStyles();
const theme = useTheme();
const handleVehiclesChange = (event) => {
setSelectedVehicles(event.target.value);
};
const onSubmit = async (event) => { const onSubmit = async (event) => {
try { try {
event.preventDefault(); event.preventDefault();
@@ -73,17 +54,12 @@ const MainForm = () => {
setMessage(e.message); setMessage(e.message);
} }
}; };
const handleCarOpen = async () => {
try {
await getVehicles(null, token);
} catch (e) {
setMessage(e.message);
}
};
useEffect(() => { useEffect(() => {
getData(); getData();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]); }, [token]);
useEffect(() => { useEffect(() => {
if (!packages || packages.length === 0) return; if (!packages || packages.length === 0) return;
var data = packages[0]; var data = packages[0];
@@ -160,43 +136,7 @@ const MainForm = () => {
fullWidth fullWidth
placeholder="Release Notes URL" placeholder="Release Notes URL"
/> />
<FormControl <CarSelection onSelection={setSelectedVehicles} />
className={classes.formControl}
variant="outlined"
fullWidth
>
<InputLabel htmlFor="vehicles">Vehicles</InputLabel>
<Select
label="Vehicles"
placeholder="Select vehicles"
id="vehicles"
name="vehicles"
multiple
className={classes.menuProps}
variant="outlined"
onOpen={handleCarOpen}
onChange={handleVehiclesChange}
value={selectedVehicles}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => (
<div className={classes.chips}>
{selected.map((value) => (
<Chip key={value} label={value} className={classes.chip} />
))}
</div>
)}
>
{vehicles.map((vehicle) => (
<MenuItem
key={vehicle.vin}
value={vehicle.vin}
style={menuItemStyle(vehicle, selectedVehicles, theme)}
>
{vehicle.vin}
</MenuItem>
))}
</Select>
</FormControl>
<Button <Button
type="submit" type="submit"
disabled={busy} disabled={busy}
@@ -214,11 +154,9 @@ const MainForm = () => {
}; };
const UpdatePackageDeployForm = () => ( const UpdatePackageDeployForm = () => (
<VehicleProvider>
<UpdatesProvider> <UpdatesProvider>
<MainForm /> <MainForm />
</UpdatesProvider> </UpdatesProvider>
</VehicleProvider>
); );
export default UpdatePackageDeployForm; export default UpdatePackageDeployForm;

View File

@@ -0,0 +1,136 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { FormControl, InputLabel, Select } from "@material-ui/core";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import useStyles from "../../useStyles";
const Control = (props) => {
const classes = useStyles();
const {
models,
years,
vehicles,
getModels,
getYears,
getVehicles,
} = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [model, setModel] = useState("");
const [year, setYear] = useState(-1);
const handleChangeModel = (event) => {
setModel(event.target.value);
};
const handleChangeYear = (event) => {
setYear(event.target.value);
};
useEffect(() => {
if (!token) return;
(async () => {
try {
await getModels(token);
await getYears(token);
} catch (e) {}
})();
// eslint-disable-next-line
}, [token]);
useEffect(() => {
if (!models || models.length === 0) return;
setModel(models[0]);
}, [models]);
useEffect(() => {
if (!years || years.length === 0) return;
setYear(years[0]);
}, [years]);
useEffect(() => {
if (model === null || year === -1) return;
getVehicles({ model, year }, token);
// eslint-disable-next-line
}, [model, year]);
useEffect(() => {
if (!props.onSelection) return;
const vins = vehicles.map((item) => item.vin);
props.onSelection(vins);
// eslint-disable-next-line
}, [vehicles]);
return (
<div className={classes.form}>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-model" style={{ backgroundColor: "White" }}>
Model
</InputLabel>
<Select
native
value={model}
onChange={handleChangeModel}
variant="outlined"
inputProps={{
name: "car-model",
id: "car-model",
}}
>
{models.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-year" style={{ backgroundColor: "White" }}>
Year
</InputLabel>
<Select
native
value={year}
onChange={handleChangeYear}
variant="outlined"
inputProps={{
name: "car-year",
id: "car-year",
}}
>
{years.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<div className={classes.labelInline}>
{vehicles.length === 0
? "No Cars Selected"
: vehicles.length === 1
? "1 Car Selected"
: `${vehicles.length} Cars Selected`}
</div>
</div>
);
};
const CarSelection = (props) => (
<VehicleProvider>
<Control {...props} />
</VehicleProvider>
);
CarSelection.propTypes = {
onSelection: PropTypes.func,
};
export default CarSelection;

View File

@@ -226,6 +226,6 @@ const validateCreateCarUpdates = (data) => {
} }
if (!data.vins || data.vins.length === 0) { if (!data.vins || data.vins.length === 0) {
throw new Error("Car ids required"); throw new Error("Cars are required");
} }
}; };

View File

@@ -273,7 +273,7 @@ describe("UpdatesContext", () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false") expect(screen.getByTestId("busy").innerHTML).toBe("false")
); );
checkState("false", "Car ids required", null); checkState("false", "Cars are required", null);
}); });
it("with-good-data", async () => { it("with-good-data", async () => {

View File

@@ -29,6 +29,8 @@ export const VehicleProvider = ({ children }) => {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [vehicles, setVehicles] = useState([]); const [vehicles, setVehicles] = useState([]);
const [totalVehicles, setTotalVehicles] = useState(0); const [totalVehicles, setTotalVehicles] = useState(0);
const [models, setModels] = useState([]);
const [years, setYears] = useState([]);
const getVehicles = async (search, token) => { const getVehicles = async (search, token) => {
try { try {
@@ -60,14 +62,40 @@ export const VehicleProvider = ({ children }) => {
} }
}; };
const getModels = async (token) => {
try {
setBusy(true);
const result = await api.getModels(token);
if (result.error) throw new Error(`Get models error. ${result.message}`);
setModels(result.data);
} finally {
setBusy(false);
}
};
const getYears = async (token) => {
try {
setBusy(true);
const result = await api.getYears(token);
if (result.error) throw new Error(`Get years error. ${result.message}`);
setYears(result.data);
} finally {
setBusy(false);
}
};
return ( return (
<VehicleContext.Provider <VehicleContext.Provider
value={{ value={{
busy, busy,
vehicles, vehicles,
totalVehicles, totalVehicles,
models,
years,
getVehicles, getVehicles,
addVehicle, addVehicle,
getModels,
getYears,
}} }}
> >
{children} {children}

View File

@@ -2,6 +2,8 @@ import React from "react";
let busy = false; let busy = false;
let vehicles = []; let vehicles = [];
let models = ["Ocean", "PEAR"];
let years = [2023, 2024];
let totalVehicles = 0; let totalVehicles = 0;
let error = null; let error = null;
@@ -13,8 +15,16 @@ export const useVehicleContext = () => ({
busy, busy,
vehicles, vehicles,
totalVehicles, totalVehicles,
models,
years,
getVehicles: jest.fn(() => vehicles), getVehicles: jest.fn(() => vehicles),
addVehicle: jest.fn(), addVehicle: jest.fn(),
getModels: jest.fn(() => {
models = ["Ocean", "PEAR"];
}),
getYears: jest.fn(() => {
years = [2023, 2024];
}),
}); });
export const setBusy = (val) => { export const setBusy = (val) => {

View File

@@ -16,7 +16,7 @@ const menuData = [
roles: [Roles.CREATE, Roles.READ], roles: [Roles.CREATE, Roles.READ],
}, },
{ {
label: "Create Packages", label: "Create Package",
to: "/package-upload", to: "/package-upload",
roles: [Roles.CREATE], roles: [Roles.CREATE],
}, },
@@ -26,7 +26,7 @@ const menuData = [
roles: [Roles.CREATE, Roles.READ], roles: [Roles.CREATE, Roles.READ],
}, },
{ {
label: "Add Vehicles", label: "Add Vehicle",
to: "/vehicle-add", to: "/vehicle-add",
roles: [Roles.CREATE], roles: [Roles.CREATE],
}, },

View File

@@ -66,7 +66,7 @@ exports[`SideMenu Authenticated 1`] = `
<span <span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
> >
Create Packages Create Package
</span> </span>
</div> </div>
<span <span
@@ -110,7 +110,7 @@ exports[`SideMenu Authenticated 1`] = `
<span <span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
> >
Add Vehicles Add Vehicle
</span> </span>
</div> </div>
<span <span

View File

@@ -51,10 +51,10 @@ exports[`File Upload Form Should render 1`] = `
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline" class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
> >
<legend <legend
class="PrivateNotchedOutline-legendLabelled-26" class="PrivateNotchedOutline-legendLabelled-28"
> >
<span> <span>
Package name Package name
@@ -97,10 +97,10 @@ exports[`File Upload Form Should render 1`] = `
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline" class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
> >
<legend <legend
class="PrivateNotchedOutline-legendLabelled-26" class="PrivateNotchedOutline-legendLabelled-28"
> >
<span> <span>
Version Version
@@ -143,10 +143,10 @@ exports[`File Upload Form Should render 1`] = `
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline" class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
> >
<legend <legend
class="PrivateNotchedOutline-legendLabelled-26" class="PrivateNotchedOutline-legendLabelled-28"
> >
<span> <span>
Description Description
@@ -190,10 +190,10 @@ exports[`File Upload Form Should render 1`] = `
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline" class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
> >
<legend <legend
class="PrivateNotchedOutline-legendLabelled-26" class="PrivateNotchedOutline-legendLabelled-28"
> >
<span> <span>
Release Notes URL Release Notes URL

View File

@@ -39,6 +39,17 @@ const useStyles = makeStyles((theme) => ({
width: "100%", width: "100%",
minWidth: 120, minWidth: 120,
}, },
formControlInline: {
marginRight: "10px !important",
minWidth: 120,
},
labelInline: {
fontSize: "1.25em",
margin: theme.spacing(2, 0, 1),
display: "inline-flex",
boxSizing: "border-box",
position: "relative",
},
chips: { chips: {
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",

View File

@@ -15,6 +15,16 @@ const vehiclesAPI = {
data.push(vehicle); data.push(vehicle);
return vehicle; return vehicle;
}, },
getModels: async (token) => {
return {
data: ["Ocean", "Pear"],
};
},
getYears: async (token) => {
return {
data: [2021, 2022],
};
},
}; };
export default vehiclesAPI; export default vehiclesAPI;

View File

@@ -15,10 +15,21 @@ const vehiclesAPI = {
return fetch(u, { return fetch(u, {
method: "GET", method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)), headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
}) })
.then(fetchRespHandler); .then(fetchRespHandler)
}, },
getModels: async (token) => fetch(`${API_ENDPOINT}/vehiclemodels`, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler),
getYears: async (token) => fetch(`${API_ENDPOINT}/vehicleyears`, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler),
}; };
export default vehiclesAPI; export default vehiclesAPI;