Sync to main (#26)
* Fix sign up form bug * Add run.sh to run setup and run web app * Output node version * Update readme with run.sh * Fix file upload form to handle ota_update service * Enable file upload form Enable error boundary to catch React errors (#7) Fix warning for link noreferrer Include authorization header with file upload * Remove default localhost settings (#8) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix json link format and remove localhost default settings (#10) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix link data format * Fix link json again (#12) Use id token instead of access token * nginx things * Web Worker Sign Out and Use Go API (#13) * Calculate checksum and send with file upload * Limit file upload and display rejected file error * Add sign in timeout * Check auth token structure before setting Clean up * Use web worker timer to sign out Remove checksum Point to Go ota update * Remove checksum dependency * Use compute auth service and fix static code analyzer warnings (#15) * Clean up formatting * Use new compute_auth service Implment SSO Implement token refresh Clean up unit tests * Fix unit tests * Fix auth test Fix warnings * Update default settings for compute_auth * Change main UI layout and add VINs to add and upload forms (#16) * Add new upload update package form Add new add vehicle form Add new side menu layout Add new toolbar layout Update and add unit tests * Enable add get and add vehicles * Integration issues with ota_update service * Update get vehicle JSON format * Fix related unit test Add release notes field * Add StatusContext to display error and status messages * Handle api error json (#18) * Handle api error json * Fix get vehicles error handling Update .env.template * Fix signout refresh (#20) * Merge to main (#17) * Fix sign up form bug * Add run.sh to run setup and run web app * Output node version * Update readme with run.sh * Fix file upload form to handle ota_update service * Enable file upload form Enable error boundary to catch React errors (#7) Fix warning for link noreferrer Include authorization header with file upload * Remove default localhost settings (#8) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix json link format and remove localhost default settings (#10) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix link data format * Fix link json again (#12) Use id token instead of access token * nginx things * Web Worker Sign Out and Use Go API (#13) * Calculate checksum and send with file upload * Limit file upload and display rejected file error * Add sign in timeout * Check auth token structure before setting Clean up * Use web worker timer to sign out Remove checksum Point to Go ota update * Remove checksum dependency * Use compute auth service and fix static code analyzer warnings (#15) * Clean up formatting * Use new compute_auth service Implment SSO Implement token refresh Clean up unit tests * Fix unit tests * Fix auth test Fix warnings * Update default settings for compute_auth * Change main UI layout and add VINs to add and upload forms (#16) * Add new upload update package form Add new add vehicle form Add new side menu layout Add new toolbar layout Update and add unit tests * Enable add get and add vehicles * Integration issues with ota_update service * Update get vehicle JSON format * Fix related unit test Add release notes field * Add StatusContext to display error and status messages * Handle api error json (#18) * Handle api error json * Fix get vehicles error handling Update .env.template Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com> * Fix sign out and refresh * Check for bad json Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com> * Add role checks (#21) * Add role checks * Remove moved Roles enum * Add package updates, car updates, and vehicle screens (#25) Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com>
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -7361,9 +7361,9 @@
|
|||||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||||
},
|
},
|
||||||
"immer": {
|
"immer": {
|
||||||
"version": "7.0.9",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
|
||||||
"integrity": "sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A=="
|
"integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
|
||||||
},
|
},
|
||||||
"import-cwd": {
|
"import-cwd": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -9969,9 +9969,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"open": {
|
"open": {
|
||||||
"version": "7.4.0",
|
"version": "7.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/open/-/open-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||||
"integrity": "sha512-PGoBCX/lclIWlpS/R2PQuIR4NJoXh6X5AwVzE7WXnWRGvHg7+4TBCgsujUgiPpm0K1y4qvQeWnCWVTpTKZBtvA==",
|
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0",
|
"is-docker": "^2.0.0",
|
||||||
"is-wsl": "^2.1.1"
|
"is-wsl": "^2.1.1"
|
||||||
@@ -11697,9 +11697,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-dev-utils": {
|
"react-dev-utils": {
|
||||||
"version": "11.0.2",
|
"version": "11.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz",
|
||||||
"integrity": "sha512-xG7GlMoYkrgc2M1kDCHKRywXMDbFnjOB+/VzpytQyYBusEzR8NlGTMmUbvN86k94yyKu5XReHB8eZC2JZrNchQ==",
|
"integrity": "sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/code-frame": "7.10.4",
|
"@babel/code-frame": "7.10.4",
|
||||||
"address": "1.1.2",
|
"address": "1.1.2",
|
||||||
@@ -11714,7 +11714,7 @@
|
|||||||
"global-modules": "2.0.0",
|
"global-modules": "2.0.0",
|
||||||
"globby": "11.0.1",
|
"globby": "11.0.1",
|
||||||
"gzip-size": "5.1.1",
|
"gzip-size": "5.1.1",
|
||||||
"immer": "7.0.9",
|
"immer": "8.0.1",
|
||||||
"is-root": "2.1.0",
|
"is-root": "2.1.0",
|
||||||
"loader-utils": "2.0.0",
|
"loader-utils": "2.0.0",
|
||||||
"open": "^7.0.2",
|
"open": "^7.0.2",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ jest.mock("../Contexts/VehicleContext");
|
|||||||
|
|
||||||
import { render, screen, cleanup, waitForElementToBeRemoved } from "@testing-library/react";
|
import { render, screen, cleanup, waitForElementToBeRemoved } from "@testing-library/react";
|
||||||
import { setToken } from "../Contexts/UserContext";
|
import { setToken } from "../Contexts/UserContext";
|
||||||
|
import { TEST_AUTH_OBJECT } from "../../utils/testing"
|
||||||
import App from ".";
|
import App from ".";
|
||||||
|
|
||||||
const TEST_TOKEN = { idToken: { jwtToken: "TEST" } };
|
|
||||||
const LOADING_STATUS = "Loading...";
|
const LOADING_STATUS = "Loading...";
|
||||||
|
|
||||||
const renderRoute = async (route) => {
|
const renderRoute = async (route) => {
|
||||||
@@ -37,6 +37,12 @@ describe("App", () => {
|
|||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Route /package-upload unauthenticated", async () => {
|
||||||
|
const container = await renderRoute("/package-upload");
|
||||||
|
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it("Route /vehicle-add unauthenticated", async () => {
|
it("Route /vehicle-add unauthenticated", async () => {
|
||||||
const container = await renderRoute("/vehicle-add");
|
const container = await renderRoute("/vehicle-add");
|
||||||
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
||||||
@@ -44,21 +50,28 @@ describe("App", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Route / authenticated", async () => {
|
it("Route / authenticated", async () => {
|
||||||
setToken(TEST_TOKEN);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
const container = await renderRoute("/");
|
const container = await renderRoute("/");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Upload Update Package");
|
expect(container.querySelector("h1").innerHTML).toEqual("Welcome John!");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /home authenticated", async () => {
|
it("Route /home authenticated", async () => {
|
||||||
setToken(TEST_TOKEN);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
const container = await renderRoute("/home");
|
const container = await renderRoute("/home");
|
||||||
|
expect(container.querySelector("h1").innerHTML).toEqual("Welcome John!");
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Route /package-upload authenticated", async () => {
|
||||||
|
setToken(TEST_AUTH_OBJECT);
|
||||||
|
const container = await renderRoute("/package-upload");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Upload Update Package");
|
expect(container.querySelector("h1").innerHTML).toEqual("Upload Update Package");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /vehicle-add authenticated", async () => {
|
it("Route /vehicle-add authenticated", async () => {
|
||||||
setToken(TEST_TOKEN);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
const container = await renderRoute("/vehicle-add");
|
const container = await renderRoute("/vehicle-add");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Add Vehicle");
|
expect(container.querySelector("h1").innerHTML).toEqual("Add Vehicle");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
@@ -71,7 +84,7 @@ describe("App", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Route /page-not-found authenticated", async () => {
|
it("Route /page-not-found authenticated", async () => {
|
||||||
setToken(TEST_TOKEN);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
const container = await renderRoute("/page-not-found");
|
const container = await renderRoute("/page-not-found");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
|
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
225
src/components/CarUpdates/Deploy/index.jsx
Normal file
225
src/components/CarUpdates/Deploy/index.jsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdatesProvider,
|
||||||
|
useUpdatesContext,
|
||||||
|
} from "../../Contexts/UpdatesContext";
|
||||||
|
import {
|
||||||
|
useVehicleContext,
|
||||||
|
VehicleProvider,
|
||||||
|
} from "../../Contexts/VehicleContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
import { tsLocalDateTimeString } from "../../../utils/dates";
|
||||||
|
import menuItemStyle from "../../menuItemStyle";
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const { packageid } = useParams();
|
||||||
|
const { getPackages, createCarUpdates, packages, busy } = useUpdatesContext();
|
||||||
|
const { getVehicles, vehicles } = useVehicleContext();
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const [packageName, setPackageName] = useState("");
|
||||||
|
const [version, setVersion] = useState("");
|
||||||
|
const [link, setLink] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [releaseNotesLink, setReleaseNotesLink] = useState("");
|
||||||
|
const [createDate, setCreateDate] = useState("");
|
||||||
|
const [selectedVehicles, setSelectedVehicles] = useState([]);
|
||||||
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
const handleVehiclesChange = (event) => {
|
||||||
|
setSelectedVehicles(event.target.value);
|
||||||
|
};
|
||||||
|
const getCarIDs = () => {
|
||||||
|
if (!selectedVehicles) return [];
|
||||||
|
return selectedVehicles.map((vin) => {
|
||||||
|
return vehicles.find((vehicle) => vehicle.vin === vin).id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onSubmit = async (event) => {
|
||||||
|
try {
|
||||||
|
event.preventDefault();
|
||||||
|
const data = {
|
||||||
|
package_id: parseInt(packageid),
|
||||||
|
car_ids: getCarIDs(),
|
||||||
|
};
|
||||||
|
await createCarUpdates(data, token);
|
||||||
|
setMessage(
|
||||||
|
`Deployed ${packageName} ${version} to ${selectedVehicles.length} cars`
|
||||||
|
);
|
||||||
|
setSelectedVehicles([]);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getData = async () => {
|
||||||
|
try {
|
||||||
|
getPackages({ id: parseInt(packageid) }, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleCarOpen = async () => {
|
||||||
|
try {
|
||||||
|
await getVehicles(null, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
getData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [token]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!packages || packages.length === 0) return;
|
||||||
|
var data = packages[0];
|
||||||
|
|
||||||
|
setPackageName(data.package_name);
|
||||||
|
setVersion(data.version);
|
||||||
|
setLink(data.link);
|
||||||
|
setDescription(data.desc || "");
|
||||||
|
setReleaseNotesLink(data.release_notes || "");
|
||||||
|
setCreateDate(tsLocalDateTimeString(data.timestamp));
|
||||||
|
}, [packages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
{`[${packageid}] ${packageName} ${version}`}
|
||||||
|
</Typography>
|
||||||
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
|
<TextField
|
||||||
|
label="Create Date"
|
||||||
|
variant="filled"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
value={createDate}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="link"
|
||||||
|
name="link"
|
||||||
|
label="Package link"
|
||||||
|
variant="filled"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
value={link}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
variant="filled"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
value={description}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
placeholder="Package description"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="releasenotes"
|
||||||
|
name="releasenotes"
|
||||||
|
label="Release Notes URL"
|
||||||
|
variant="filled"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
value={releaseNotesLink}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Release Notes URL"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
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
|
||||||
|
type="submit"
|
||||||
|
disabled={busy}
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
{busy ? "Deploying..." : "Deploy"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdatePackageDeployForm = () => (
|
||||||
|
<VehicleProvider>
|
||||||
|
<UpdatesProvider>
|
||||||
|
<MainForm />
|
||||||
|
</UpdatesProvider>
|
||||||
|
</VehicleProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UpdatePackageDeployForm;
|
||||||
139
src/components/CarUpdates/Status/index.jsx
Normal file
139
src/components/CarUpdates/Status/index.jsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TablePagination,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdatesProvider,
|
||||||
|
useUpdatesContext,
|
||||||
|
} from "../../Contexts/UpdatesContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
import { LocalDateTimeString } from "../../../utils/dates";
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const { packageid } = useParams();
|
||||||
|
const classes = useStyles();
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
const {
|
||||||
|
getCarUpdates,
|
||||||
|
carUpdates,
|
||||||
|
totalCarUpdates,
|
||||||
|
getPackages,
|
||||||
|
packages,
|
||||||
|
} = useUpdatesContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
getPackages({ id: packageid }, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
getCarUpdates(
|
||||||
|
{
|
||||||
|
packageid,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: pageSize * pageIndex,
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [pageIndex, pageSize, token]);
|
||||||
|
|
||||||
|
const handleChangePageIndex = (event, newIndex) => {
|
||||||
|
setPageIndex(newIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePageSize = (event) => {
|
||||||
|
setPageSize(parseInt(event.target.value, 10));
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
{packages &&
|
||||||
|
packages.length > 0 &&
|
||||||
|
`${packages[0].package_name} ${packages[0].version}`}
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center">ID</TableCell>
|
||||||
|
<TableCell align="center">Vehicle</TableCell>
|
||||||
|
<TableCell align="center">Status</TableCell>
|
||||||
|
<TableCell align="center">Created</TableCell>
|
||||||
|
<TableCell align="center">Updated</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{carUpdates.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
<TableCell align="center">{row.id}</TableCell>
|
||||||
|
<TableCell align="center">{`${row.car.vin} ${row.car.model} ${row.car.year}`}</TableCell>
|
||||||
|
<TableCell align="center">{row.status}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{LocalDateTimeString(row.created)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{LocalDateTimeString(row.updated)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
colSpan={5}
|
||||||
|
count={totalCarUpdates}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
page={pageIndex}
|
||||||
|
SelectProps={{
|
||||||
|
inputProps: { "aria-label": "rows per page" },
|
||||||
|
native: true,
|
||||||
|
}}
|
||||||
|
onChangePage={handleChangePageIndex}
|
||||||
|
onChangeRowsPerPage={handleChangePageSize}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CarUpdatesStatus = () => (
|
||||||
|
<UpdatesProvider>
|
||||||
|
<MainForm />
|
||||||
|
</UpdatesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CarUpdatesStatus;
|
||||||
@@ -1,30 +1,38 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
import useStyles from "../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext";
|
import {
|
||||||
import { useStatusContext } from "../Contexts/StatusContext";
|
useVehicleContext,
|
||||||
import { useUserContext } from "../Contexts/UserContext";
|
VehicleProvider,
|
||||||
|
} from "../../Contexts/VehicleContext";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import { Button, TextField, Typography } from "@material-ui/core";
|
import { Button, TextField, Typography } from "@material-ui/core";
|
||||||
|
|
||||||
const MainForm = () => {
|
const MainForm = () => {
|
||||||
const { addVehicle, busy } = useVehicleContext();
|
const { addVehicle, busy } = useVehicleContext();
|
||||||
const { setMessage } = useStatusContext();
|
const { setMessage } = useStatusContext();
|
||||||
const { token } = useUserContext();
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const vinEl = useRef(null);
|
const vinEl = useRef(null);
|
||||||
|
const modelEl = useRef(null);
|
||||||
|
const yearEl = useRef(null);
|
||||||
|
|
||||||
const onSubmit = async (event) => {
|
const onSubmit = async (event) => {
|
||||||
try {
|
try {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const {
|
|
||||||
idToken: { jwtToken: authToken },
|
|
||||||
} = token;
|
|
||||||
const formData = {
|
const formData = {
|
||||||
vin: vinEl.current.value,
|
vin: vinEl.current.value,
|
||||||
|
model: modelEl.current.value,
|
||||||
|
year: parseInt(yearEl.current.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await addVehicle(formData, authToken);
|
const result = await addVehicle(formData, token);
|
||||||
|
|
||||||
setMessage(`Added ${result.vin}`);
|
setMessage(`Added ${result.vin}`);
|
||||||
vinEl.current.value = "";
|
vinEl.current.value = "";
|
||||||
@@ -52,6 +60,36 @@ const MainForm = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
inputRef={vinEl}
|
inputRef={vinEl}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
id="model"
|
||||||
|
name="model"
|
||||||
|
label="Model"
|
||||||
|
defaultValue="Ocean"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "255",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
inputRef={modelEl}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="year"
|
||||||
|
name="year"
|
||||||
|
label="Year"
|
||||||
|
type="number"
|
||||||
|
defaultValue="2022"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "4",
|
||||||
|
minLength: "4",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
inputRef={yearEl}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
121
src/components/Cars/List/index.jsx
Normal file
121
src/components/Cars/List/index.jsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TablePagination,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useVehicleContext,
|
||||||
|
VehicleProvider,
|
||||||
|
} from "../../Contexts/VehicleContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
import { LocalDateTimeString } from "../../../utils/dates";
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [pageSize, setPageSize] = useState(5);
|
||||||
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
const { getVehicles, vehicles, totalVehicles } = useVehicleContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
getVehicles(
|
||||||
|
{
|
||||||
|
limit: pageSize,
|
||||||
|
offset: pageSize * pageIndex,
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [pageIndex, pageSize, token]);
|
||||||
|
|
||||||
|
const handleChangePageIndex = (event, newIndex) => {
|
||||||
|
setPageIndex(newIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePageSize = (event) => {
|
||||||
|
setPageSize(parseInt(event.target.value, 10));
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Vehicles
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center">ID</TableCell>
|
||||||
|
<TableCell align="center">VIN</TableCell>
|
||||||
|
<TableCell align="center">Model</TableCell>
|
||||||
|
<TableCell align="center">Year</TableCell>
|
||||||
|
<TableCell align="center">Created</TableCell>
|
||||||
|
<TableCell align="center">Updated</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{vehicles.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
<TableCell align="center">{row.id}</TableCell>
|
||||||
|
<TableCell align="center">{row.vin}</TableCell>
|
||||||
|
<TableCell align="center">{row.model}</TableCell>
|
||||||
|
<TableCell align="center">{row.year}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{LocalDateTimeString(row.created)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{LocalDateTimeString(row.updated)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
colSpan={6}
|
||||||
|
count={totalVehicles}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
page={pageIndex}
|
||||||
|
SelectProps={{
|
||||||
|
inputProps: { "aria-label": "rows per page" },
|
||||||
|
native: true,
|
||||||
|
}}
|
||||||
|
onChangePage={handleChangePageIndex}
|
||||||
|
onChangeRowsPerPage={handleChangePageSize}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VehiclesList = () => (
|
||||||
|
<VehicleProvider>
|
||||||
|
<MainForm />
|
||||||
|
</VehicleProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default VehiclesList;
|
||||||
@@ -38,10 +38,6 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
throw new Error("Package update version required");
|
throw new Error("Package update version required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.vehicles || formData.vehicles.length === 0) {
|
|
||||||
throw new Error("Vehicles required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uploadFiles || uploadFiles.length === 0) {
|
if (!uploadFiles || uploadFiles.length === 0) {
|
||||||
throw new Error("File required");
|
throw new Error("File required");
|
||||||
}
|
}
|
||||||
@@ -64,7 +60,7 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
setStatus(`Uploading ${filename}`);
|
setStatus(`Uploading ${filename}`);
|
||||||
setCancelUpload(getCancelToken());
|
setCancelUpload(getCancelToken());
|
||||||
|
|
||||||
const { data } = await uploadFile(
|
const data = await uploadFile(
|
||||||
file,
|
file,
|
||||||
formData,
|
formData,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ describe("FileUploadContext", () => {
|
|||||||
linkURL,
|
linkURL,
|
||||||
upload,
|
upload,
|
||||||
cancel,
|
cancel,
|
||||||
setFiles,
|
|
||||||
} = useFileUploadContext();
|
} = useFileUploadContext();
|
||||||
const { message, setMessage } = useStatusContext();
|
const { message, setMessage } = useStatusContext();
|
||||||
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
|
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
|
||||||
|
|||||||
291
src/components/Contexts/UpdateContext.test.jsx
Normal file
291
src/components/Contexts/UpdateContext.test.jsx
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
jest.mock("../../services/updates");
|
||||||
|
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
cleanup,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
import { UpdatesProvider, useUpdatesContext } from "../Contexts/UpdatesContext";
|
||||||
|
import { StatusProvider, useStatusContext } from "../Contexts/StatusContext";
|
||||||
|
import { TEST_AUTH_OBJECT } from "../../utils/testing";
|
||||||
|
|
||||||
|
describe("UpdatesContext", () => {
|
||||||
|
describe("getPackages", () => {
|
||||||
|
const expectedData = `[{"id":1,"package_name":"Test","version":"1.0","link":"http://cloudfront.com/download"},{"id":2,"package_name":"Test","version":"1.1","link":"http://cloudfront.com/download"},{"id":3,"package_name":"Test","version":"1.2","link":"http://cloudfront.com/download"}]`;
|
||||||
|
const checkState = (busy, packages, message) => {
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||||
|
expect(screen.getByTestId("packages").innerHTML).toEqual(packages);
|
||||||
|
expect(screen.getByTestId("message").innerHTML).toEqual(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const TestComp = () => {
|
||||||
|
const { busy, packages, getPackages } = useUpdatesContext();
|
||||||
|
const { message, setMessage } = useStatusContext();
|
||||||
|
const exec = async (data, token) => {
|
||||||
|
try {
|
||||||
|
await getPackages(data, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="busy">{busy.toString()}</div>
|
||||||
|
<div data-testid="packages">{JSON.stringify(packages)}</div>
|
||||||
|
<div data-testid="message">{message}</div>
|
||||||
|
<button
|
||||||
|
data-testid="no-filter"
|
||||||
|
onClick={() => {
|
||||||
|
exec(null, TEST_AUTH_OBJECT);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="with-filter"
|
||||||
|
onClick={() => {
|
||||||
|
exec({ package_name: "Test" }, TEST_AUTH_OBJECT);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<StatusProvider>
|
||||||
|
<UpdatesProvider>
|
||||||
|
<TestComp />
|
||||||
|
</UpdatesProvider>
|
||||||
|
</StatusProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initial state", async () => {
|
||||||
|
checkState("false", "[]", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getPackages no filter", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("no-filter"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", expectedData, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getPackages with filter", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("with-filter"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", expectedData, "");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updatePackage", () => {
|
||||||
|
let result = null;
|
||||||
|
const checkState = (busy, message, data) => {
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||||
|
expect(screen.getByTestId("message").innerHTML).toEqual(message);
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const TestComp = () => {
|
||||||
|
const { busy, updatePackage } = useUpdatesContext();
|
||||||
|
const { message, setMessage } = useStatusContext();
|
||||||
|
const exec = async (data, token) => {
|
||||||
|
var r = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
r = await updatePackage(data, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="busy">{busy.toString()}</div>
|
||||||
|
<div data-testid="message">{message}</div>
|
||||||
|
<button
|
||||||
|
data-testid="no-data"
|
||||||
|
onClick={async () => {
|
||||||
|
result = await exec(null, TEST_AUTH_OBJECT);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="with-bad-data"
|
||||||
|
onClick={async () => {
|
||||||
|
result = await exec({ package_name: "Test" }, TEST_AUTH_OBJECT);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="with-good-data"
|
||||||
|
onClick={async () => {
|
||||||
|
result = await exec(
|
||||||
|
{
|
||||||
|
package_name: "Test",
|
||||||
|
version: "1.0",
|
||||||
|
},
|
||||||
|
TEST_AUTH_OBJECT
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<StatusProvider>
|
||||||
|
<UpdatesProvider>
|
||||||
|
<TestComp />
|
||||||
|
</UpdatesProvider>
|
||||||
|
</StatusProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
result = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initial state", () => {
|
||||||
|
checkState("false", "", null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no-data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("no-data"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", "No update data", null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with-bad-data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("with-bad-data"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", "Version required", null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with-good-data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("with-good-data"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", "", { package_name: "Test", version: "1.0" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCarUpdates", () => {
|
||||||
|
let result = null;
|
||||||
|
const checkState = (busy, message, data) => {
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||||
|
expect(screen.getByTestId("message").innerHTML).toEqual(message);
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const TestComp = () => {
|
||||||
|
const { busy, createCarUpdates } = useUpdatesContext();
|
||||||
|
const { message, setMessage } = useStatusContext();
|
||||||
|
const exec = async (data, token) => {
|
||||||
|
var r = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
r = await createCarUpdates(data, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="busy">{busy.toString()}</div>
|
||||||
|
<div data-testid="message">{message}</div>
|
||||||
|
<button
|
||||||
|
data-testid="no-data"
|
||||||
|
onClick={async () => {
|
||||||
|
result = await exec(null, TEST_AUTH_OBJECT);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="with-bad-data"
|
||||||
|
onClick={async () => {
|
||||||
|
result = await exec(
|
||||||
|
{ package_id: 1, car_ids: [] },
|
||||||
|
TEST_AUTH_OBJECT
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="with-good-data"
|
||||||
|
onClick={async () => {
|
||||||
|
result = await exec(
|
||||||
|
{
|
||||||
|
package_id: 1,
|
||||||
|
car_ids: [1, 2, 3],
|
||||||
|
},
|
||||||
|
TEST_AUTH_OBJECT
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<StatusProvider>
|
||||||
|
<UpdatesProvider>
|
||||||
|
<TestComp />
|
||||||
|
</UpdatesProvider>
|
||||||
|
</StatusProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
result = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initial state", () => {
|
||||||
|
checkState("false", "", null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no-data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("no-data"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", "No car update data", null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with-bad-data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("with-bad-data"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", "Car ids required", null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with-good-data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("with-good-data"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toBe("false")
|
||||||
|
);
|
||||||
|
checkState("false", "", {
|
||||||
|
id: 1,
|
||||||
|
package_id: 1,
|
||||||
|
car_ids: [1, 2, 3],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
131
src/components/Contexts/UpdatesContext.jsx
Normal file
131
src/components/Contexts/UpdatesContext.jsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
|
||||||
|
import api from "../../services/updates";
|
||||||
|
|
||||||
|
const UpdatesContext = React.createContext();
|
||||||
|
|
||||||
|
export const UpdatesProvider = ({ children }) => {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [packages, setPackages] = useState([]);
|
||||||
|
const [carUpdates, setCarUpdates] = useState([]);
|
||||||
|
const [totalPackages, setTotalPackages] = useState(0);
|
||||||
|
const [totalCarUpdates, setTotalCarUpdates] = useState(0);
|
||||||
|
|
||||||
|
const getPackages = async (search, token) => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
result = await api.getPackages(search, token);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Get packages error. ${result.message}`);
|
||||||
|
setPackages(result.data);
|
||||||
|
if (search && search.offset === 0 && result.total) {
|
||||||
|
setTotalPackages(result.total);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePackage = async (data, token) => {
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
validateUpdatePackage(data);
|
||||||
|
result = await api.updatePackage(data, token);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Update package error. ${result.message}`);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCarUpdates = async (data, token) => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
validateCreateCarUpdates(data);
|
||||||
|
result = await api.createCarUpdates(data, token);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Create car updates error. ${result.message}`);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCarUpdates = async (search, token) => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
result = await api.getCarUpdates(search, token);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`Get packages error. ${result.message}`);
|
||||||
|
setCarUpdates(result.data);
|
||||||
|
if (search && search.offset === 0 && result.total) {
|
||||||
|
setTotalCarUpdates(result.total);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UpdatesContext.Provider
|
||||||
|
value={{
|
||||||
|
busy,
|
||||||
|
packages,
|
||||||
|
totalPackages,
|
||||||
|
carUpdates,
|
||||||
|
totalCarUpdates,
|
||||||
|
getPackages,
|
||||||
|
updatePackage,
|
||||||
|
createCarUpdates,
|
||||||
|
getCarUpdates,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</UpdatesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatesContext = () => useContext(UpdatesContext);
|
||||||
|
|
||||||
|
const validateUpdatePackage = (data) => {
|
||||||
|
if (data === null) {
|
||||||
|
throw new Error("No update data");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.package_name) {
|
||||||
|
throw new Error("Package name required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.version) {
|
||||||
|
throw new Error("Version required");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateCreateCarUpdates = (data) => {
|
||||||
|
if (data === null) {
|
||||||
|
throw new Error("No car update data");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.package_id || data.package_id === 0) {
|
||||||
|
throw new Error("Package id required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.car_ids || data.car_ids.length === 0) {
|
||||||
|
throw new Error("Car ids required");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import auth from "../../services/auth";
|
import auth from "../../services/auth";
|
||||||
import getTimerWorker from "../../services/timer";
|
import getTimerWorker from "../../services/timer";
|
||||||
|
import { parsePayload } from "../../utils/jwt";
|
||||||
|
import { getGroups } from "../../utils/roles";
|
||||||
|
|
||||||
const UserContext = React.createContext();
|
const UserContext = React.createContext();
|
||||||
|
|
||||||
export const UserProvider = ({ children }) => {
|
export const UserProvider = ({ children }) => {
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
const [token, setToken] = useState(null);
|
const [token, setToken] = useState(null);
|
||||||
|
const [groups, setGroups] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
let timer;
|
let timer;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localStorage) return;
|
try {
|
||||||
const t = JSON.parse(localStorage.getItem("token"));
|
if (!localStorage) return;
|
||||||
if (!t || !t.idToken || !t.idToken.jwtToken) return;
|
const t = JSON.parse(localStorage.getItem("token"));
|
||||||
if (!t.idToken.payload || !t.idToken.payload.exp) return;
|
if (!t) return;
|
||||||
setToken(t);
|
if (!t.idToken || !t.idToken.jwtToken) throw new Error("Invalid token");
|
||||||
|
setToken(t);
|
||||||
|
} catch (e) {
|
||||||
|
document.location = signOut();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,21 +41,20 @@ export const UserProvider = ({ children }) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isError = (resp) => {
|
|
||||||
if (resp === null) return true;
|
|
||||||
if (resp && resp.error) return true;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startSessionTimer = () => {
|
const startSessionTimer = () => {
|
||||||
const duration = 1000 * token.idToken.payload.exp - new Date().getTime();
|
if (!token || !token.idToken || !token.idToken.jwtToken) {
|
||||||
|
throw new Error("No id token");
|
||||||
|
}
|
||||||
|
const payload = parsePayload(token.idToken.jwtToken);
|
||||||
|
if (!payload || !payload.exp) throw new Error("Bad id token payload");
|
||||||
|
const duration = 1000 * payload.exp - new Date().getTime();
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
timer = getTimerWorker();
|
timer = getTimerWorker();
|
||||||
timer.onMessage(async (e) => {
|
timer.onMessage(async (e) => {
|
||||||
if (e.data === "timeout") {
|
if (e.data === "timeout") {
|
||||||
const t = await refreshTokens();
|
const t = await refreshTokens();
|
||||||
if (!isError(t)) return;
|
if (t && !t.error) return;
|
||||||
signOut();
|
document.location = signOut();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,17 +68,16 @@ export const UserProvider = ({ children }) => {
|
|||||||
} = token;
|
} = token;
|
||||||
const result = await auth.verify(idToken);
|
const result = await auth.verify(idToken);
|
||||||
|
|
||||||
if (!result && !result.valid) {
|
if (!result || !result.valid) {
|
||||||
const t = await refreshTokens();
|
const t = await refreshTokens();
|
||||||
if (!isError(t)) return;
|
if (!t || t.error) throw new Error("Unable to refresh token");
|
||||||
signOut();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGroups(getGroups(idToken));
|
||||||
startSessionTimer();
|
startSessionTimer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
signOut();
|
|
||||||
setError(`Verify error. ${e.message}`);
|
setError(`Verify error. ${e.message}`);
|
||||||
|
document.location = signOut();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,6 +106,7 @@ export const UserProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const signOut = () => {
|
const signOut = () => {
|
||||||
|
setGroups(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
@@ -146,6 +153,7 @@ export const UserProvider = ({ children }) => {
|
|||||||
value={{
|
value={{
|
||||||
fetching,
|
fetching,
|
||||||
token,
|
token,
|
||||||
|
groups,
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
signIn,
|
signIn,
|
||||||
|
|||||||
@@ -11,15 +11,7 @@ import {
|
|||||||
import { UserProvider, useUserContext } from "../Contexts/UserContext";
|
import { UserProvider, useUserContext } from "../Contexts/UserContext";
|
||||||
import auth from "../../services/auth";
|
import auth from "../../services/auth";
|
||||||
import getTimerWorker from "../../services/timer";
|
import getTimerWorker from "../../services/timer";
|
||||||
|
import { TEST_AUTH_OBJECT, TEST_EXPECTED_GROUPS } from "../../utils/testing";
|
||||||
const TEST_TOKEN = {
|
|
||||||
idToken: {
|
|
||||||
jwtToken: "TEST",
|
|
||||||
payload: {
|
|
||||||
exp: new Date().getTime() / 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const INVALID_TOKEN_RESPONSE = {
|
const INVALID_TOKEN_RESPONSE = {
|
||||||
error: "Bad Request Error",
|
error: "Bad Request Error",
|
||||||
@@ -36,10 +28,11 @@ const setupSignInEnv = (refreshResponse, valid) => {
|
|||||||
auth.setVerifyResponse({ valid });
|
auth.setVerifyResponse({ valid });
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkBaseResults = (error, fetching, token) => {
|
const checkBaseResults = (error, fetching, token, groups) => {
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual(fetching);
|
expect(screen.getByTestId("fetching").innerHTML).toEqual(fetching);
|
||||||
expect(screen.getByTestId("token").innerHTML).toEqual(token);
|
expect(screen.getByTestId("token").innerHTML).toEqual(token);
|
||||||
|
expect(screen.getByTestId("groups").innerHTML).toEqual(groups);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkTokenResults = (timer, token) => {
|
const checkTokenResults = (timer, token) => {
|
||||||
@@ -57,13 +50,14 @@ describe("UseContext", () => {
|
|||||||
describe("Signin", () => {
|
describe("Signin", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const TestComp = () => {
|
const TestComp = () => {
|
||||||
const { signIn, error, token, fetching } = useUserContext();
|
const { signIn, error, token, groups, fetching } = useUserContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-testid="error">{error}</div>
|
<div data-testid="error">{error}</div>
|
||||||
<div data-testid="fetching">{fetching.toString()}</div>
|
<div data-testid="fetching">{fetching.toString()}</div>
|
||||||
<div data-testid="token">{JSON.stringify(token)}</div>
|
<div data-testid="token">{JSON.stringify(token)}</div>
|
||||||
|
<div data-testid="groups">{groups}</div>
|
||||||
<button data-testid="signInNoCode" onClick={() => signIn("")} />
|
<button data-testid="signInNoCode" onClick={() => signIn("")} />
|
||||||
<button
|
<button
|
||||||
data-testid="signInInvalidCode"
|
data-testid="signInInvalidCode"
|
||||||
@@ -85,13 +79,13 @@ describe("UseContext", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Initial state", () => {
|
it("Initial state", () => {
|
||||||
checkBaseResults("", "false", "null");
|
checkBaseResults("", "false", "null", "");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("No auth code", () => {
|
it("No auth code", () => {
|
||||||
fireEvent.click(screen.getByTestId("signInNoCode"));
|
fireEvent.click(screen.getByTestId("signInNoCode"));
|
||||||
|
|
||||||
checkBaseResults("", "false", "null");
|
checkBaseResults("", "false", "null", "");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Invalid auth code", async () => {
|
it("Invalid auth code", async () => {
|
||||||
@@ -103,14 +97,19 @@ describe("UseContext", () => {
|
|||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
);
|
);
|
||||||
|
|
||||||
checkBaseResults("Sign in error. Bad Request Message", "false", "null");
|
checkBaseResults(
|
||||||
|
"Sign in error. Bad Request Message",
|
||||||
|
"false",
|
||||||
|
"null",
|
||||||
|
""
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Sign in form", async () => {
|
it("Sign in form", async () => {
|
||||||
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
|
const TOKEN_STRING = JSON.stringify(TEST_AUTH_OBJECT);
|
||||||
const timer = getTimerWorker();
|
const timer = getTimerWorker();
|
||||||
|
|
||||||
setupSignInEnv(TEST_TOKEN, true);
|
setupSignInEnv(TEST_AUTH_OBJECT, true);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("signIn"));
|
fireEvent.click(screen.getByTestId("signIn"));
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ describe("UseContext", () => {
|
|||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
);
|
);
|
||||||
|
|
||||||
checkBaseResults("", "false", TOKEN_STRING);
|
checkBaseResults("", "false", TOKEN_STRING, TEST_EXPECTED_GROUPS);
|
||||||
checkTokenResults(timer, TOKEN_STRING);
|
checkTokenResults(timer, TOKEN_STRING);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -126,12 +125,20 @@ describe("UseContext", () => {
|
|||||||
describe("Signout", () => {
|
describe("Signout", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const TestComp = () => {
|
const TestComp = () => {
|
||||||
const { signIn, signOut, error, token, fetching } = useUserContext();
|
const {
|
||||||
|
signIn,
|
||||||
|
signOut,
|
||||||
|
error,
|
||||||
|
token,
|
||||||
|
groups,
|
||||||
|
fetching,
|
||||||
|
} = useUserContext();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-testid="error">{error}</div>
|
<div data-testid="error">{error}</div>
|
||||||
<div data-testid="fetching">{fetching.toString()}</div>
|
<div data-testid="fetching">{fetching.toString()}</div>
|
||||||
<div data-testid="token">{JSON.stringify(token)}</div>
|
<div data-testid="token">{JSON.stringify(token)}</div>
|
||||||
|
<div data-testid="groups">{groups}</div>
|
||||||
<button data-testid="signIn" onClick={() => signIn("TEST_CODE")} />
|
<button data-testid="signIn" onClick={() => signIn("TEST_CODE")} />
|
||||||
<button data-testid="signOut" onClick={() => signOut()} />
|
<button data-testid="signOut" onClick={() => signOut()} />
|
||||||
</>
|
</>
|
||||||
@@ -142,7 +149,7 @@ describe("UseContext", () => {
|
|||||||
<TestComp />
|
<TestComp />
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
);
|
);
|
||||||
auth.setSignInResponse(TEST_TOKEN);
|
auth.setSignInResponse(TEST_AUTH_OBJECT);
|
||||||
fireEvent.click(screen.getByTestId("signIn"));
|
fireEvent.click(screen.getByTestId("signIn"));
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
@@ -154,10 +161,9 @@ describe("UseContext", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Token cleared", () => {
|
it("Token cleared", async () => {
|
||||||
fireEvent.click(screen.getByTestId("signOut"));
|
fireEvent.click(screen.getByTestId("signOut"));
|
||||||
|
checkBaseResults("", "false", "null", "");
|
||||||
checkBaseResults("", "false", "null");
|
|
||||||
if (!localStorage) return;
|
if (!localStorage) return;
|
||||||
expect(localStorage.getItem("token")).toBeNull();
|
expect(localStorage.getItem("token")).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -166,13 +172,14 @@ describe("UseContext", () => {
|
|||||||
describe("Refresh", () => {
|
describe("Refresh", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const TestComp = () => {
|
const TestComp = () => {
|
||||||
const { refresh, error, token, fetching } = useUserContext();
|
const { refresh, error, token, groups, fetching } = useUserContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-testid="error">{error}</div>
|
<div data-testid="error">{error}</div>
|
||||||
<div data-testid="fetching">{fetching.toString()}</div>
|
<div data-testid="fetching">{fetching.toString()}</div>
|
||||||
<div data-testid="token">{JSON.stringify(token)}</div>
|
<div data-testid="token">{JSON.stringify(token)}</div>
|
||||||
|
<div data-testid="groups">{groups}</div>
|
||||||
<button data-testid="refreshNoToken" onClick={() => refresh("")} />
|
<button data-testid="refreshNoToken" onClick={() => refresh("")} />
|
||||||
<button
|
<button
|
||||||
data-testid="refreshInvalidToken"
|
data-testid="refreshInvalidToken"
|
||||||
@@ -197,12 +204,12 @@ describe("UseContext", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Initial state", () => {
|
it("Initial state", () => {
|
||||||
checkBaseResults("", "false", "null");
|
checkBaseResults("", "false", "null", "");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("No refresh token", () => {
|
it("No refresh token", () => {
|
||||||
fireEvent.click(screen.getByTestId("refreshNoToken"));
|
fireEvent.click(screen.getByTestId("refreshNoToken"));
|
||||||
checkBaseResults("Refresh error. Token required", "false", "null");
|
checkBaseResults("Refresh error. Token required", "false", "null", "");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Invalid refresh token", async () => {
|
it("Invalid refresh token", async () => {
|
||||||
@@ -213,21 +220,26 @@ describe("UseContext", () => {
|
|||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
);
|
);
|
||||||
|
|
||||||
checkBaseResults("Refresh error. Bad Request Message", "false", "null");
|
checkBaseResults(
|
||||||
|
"Refresh error. Bad Request Message",
|
||||||
|
"false",
|
||||||
|
"null",
|
||||||
|
""
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Valid refresh token", async () => {
|
it("Valid refresh token", async () => {
|
||||||
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
|
const TOKEN_STRING = JSON.stringify(TEST_AUTH_OBJECT);
|
||||||
const timer = getTimerWorker();
|
const timer = getTimerWorker();
|
||||||
|
|
||||||
setupRefreshEnv(TEST_TOKEN, true);
|
setupRefreshEnv(TEST_AUTH_OBJECT, true);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("refreshValidToken"));
|
fireEvent.click(screen.getByTestId("refreshValidToken"));
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
);
|
);
|
||||||
|
|
||||||
checkBaseResults("", "false", TOKEN_STRING);
|
checkBaseResults("", "false", TOKEN_STRING, TEST_EXPECTED_GROUPS);
|
||||||
checkTokenResults(timer, TOKEN_STRING);
|
checkTokenResults(timer, TOKEN_STRING);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,11 +15,20 @@ const validateAdd = (vehicle) => {
|
|||||||
if (vehicle.vin.length > 17) {
|
if (vehicle.vin.length > 17) {
|
||||||
throw new Error("VIN cannot be larger than 17 characters");
|
throw new Error("VIN cannot be larger than 17 characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!vehicle.model || vehicle.model.length === 0) {
|
||||||
|
throw new Error("model required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vehicle.year || vehicle.year < 2000 || vehicle.year > 9999) {
|
||||||
|
throw new Error("year required");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VehicleProvider = ({ children }) => {
|
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 getVehicles = async (search, token) => {
|
const getVehicles = async (search, token) => {
|
||||||
try {
|
try {
|
||||||
@@ -30,6 +39,9 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
throw new Error(`Get vehicles error. ${result.message}`);
|
throw new Error(`Get vehicles error. ${result.message}`);
|
||||||
} else {
|
} else {
|
||||||
setVehicles(result.data);
|
setVehicles(result.data);
|
||||||
|
if (result.total) {
|
||||||
|
setTotalVehicles(result.total);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -53,6 +65,7 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
value={{
|
value={{
|
||||||
busy,
|
busy,
|
||||||
vehicles,
|
vehicles,
|
||||||
|
totalVehicles,
|
||||||
getVehicles,
|
getVehicles,
|
||||||
addVehicle,
|
addVehicle,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ describe("VehicleContext", () => {
|
|||||||
<button data-testid="addVehiclesNoVIN" onClick={() => add({})} />
|
<button data-testid="addVehiclesNoVIN" onClick={() => add({})} />
|
||||||
<button
|
<button
|
||||||
data-testid="addVehicles"
|
data-testid="addVehicles"
|
||||||
onClick={() => add({ vin: "XXXXXXXXXXX" })}
|
onClick={() =>
|
||||||
|
add({ vin: "XXXXXXXXXXX", model: "Ocean", year: 3000 })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { getGroups } from "../../../utils/roles";
|
||||||
|
|
||||||
let token = null;
|
let token = null;
|
||||||
|
let groups = null;
|
||||||
let fetching = false;
|
let fetching = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
let signInResp = {};
|
let signInResp = {};
|
||||||
let authorizeURL = "https://cognito.com/authorize?redirect=https://example.com/callback";
|
let authorizeURL =
|
||||||
let logoutURL = "https://cognito.com/logout?redirect=https://example.com/callback";
|
"https://cognito.com/authorize?redirect=https://example.com/callback";
|
||||||
|
let logoutURL =
|
||||||
|
"https://cognito.com/logout?redirect=https://example.com/callback";
|
||||||
|
|
||||||
export const UserProvider = ({ children }) => {
|
export const UserProvider = ({ children }) => {
|
||||||
return <div data-testid="mocked-userprovider">{children}</div>;
|
return <div data-testid="mocked-userprovider">{children}</div>;
|
||||||
@@ -15,6 +20,7 @@ export const useUserContext = () => ({
|
|||||||
token,
|
token,
|
||||||
fetching,
|
fetching,
|
||||||
error,
|
error,
|
||||||
|
groups,
|
||||||
signIn: jest.fn(() => signInResp),
|
signIn: jest.fn(() => signInResp),
|
||||||
signOut: jest.fn(),
|
signOut: jest.fn(),
|
||||||
getAuthorizeURL: jest.fn(() => authorizeURL),
|
getAuthorizeURL: jest.fn(() => authorizeURL),
|
||||||
@@ -26,6 +32,11 @@ export const useUserContext = () => ({
|
|||||||
|
|
||||||
export const setToken = (val) => {
|
export const setToken = (val) => {
|
||||||
token = val;
|
token = val;
|
||||||
|
if (!val || !val.idToken || !val.idToken.jwtToken) {
|
||||||
|
groups = null;
|
||||||
|
} else {
|
||||||
|
groups = getGroups(val.idToken.jwtToken);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setFetching = (val) => {
|
export const setFetching = (val) => {
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`File Upload Form Should render 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
data-testid="mocked-vehicleprovider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid="mocked-fileuploadprovider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="makeStyles-paper-1"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="MuiTypography-root MuiTypography-h5"
|
|
||||||
>
|
|
||||||
Upload Update Package
|
|
||||||
</h1>
|
|
||||||
<form
|
|
||||||
action="{onSubmit}"
|
|
||||||
class="makeStyles-form-3"
|
|
||||||
novalidate=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
|
||||||
data-shrink="false"
|
|
||||||
for="packagename"
|
|
||||||
id="packagename-label"
|
|
||||||
>
|
|
||||||
Package name
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="packagename"
|
|
||||||
maxlength="255"
|
|
||||||
name="packagename"
|
|
||||||
required=""
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-23"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Package name
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
|
||||||
data-shrink="false"
|
|
||||||
for="version"
|
|
||||||
id="version-label"
|
|
||||||
>
|
|
||||||
Version
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="version"
|
|
||||||
maxlength="255"
|
|
||||||
name="version"
|
|
||||||
required=""
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-23"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Version
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
|
||||||
data-shrink="false"
|
|
||||||
for="description"
|
|
||||||
id="description-label"
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-multiline MuiOutlinedInput-multiline"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
aria-invalid="false"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMultiline MuiOutlinedInput-inputMultiline"
|
|
||||||
id="description"
|
|
||||||
maxlength="5120"
|
|
||||||
name="description"
|
|
||||||
placeholder="Package description"
|
|
||||||
rows="4"
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-23"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Description
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
|
||||||
data-shrink="false"
|
|
||||||
for="releasenotes"
|
|
||||||
id="releasenotes-label"
|
|
||||||
>
|
|
||||||
Release Notes URL
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="releasenotes"
|
|
||||||
maxlength="1024"
|
|
||||||
name="releasenotes"
|
|
||||||
placeholder="Release Notes URL"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-23"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Release Notes URL
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root makeStyles-formControl-5 MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
|
||||||
data-shrink="false"
|
|
||||||
for="vehicles"
|
|
||||||
>
|
|
||||||
Vehicles
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiInput-root MuiInput-underline makeStyles-menuProps-8 MuiInputBase-formControl MuiInput-formControl"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-labelledby="vehicles"
|
|
||||||
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiInput-input"
|
|
||||||
id="vehicles"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiSelect-nativeInput"
|
|
||||||
id="select-multiple-chip"
|
|
||||||
name="vehicles"
|
|
||||||
placeholder="Select vehicles"
|
|
||||||
tabindex="-1"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiDropzoneArea-root"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept=""
|
|
||||||
autocomplete="off"
|
|
||||||
style="display: none;"
|
|
||||||
tabindex="-1"
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="MuiDropzoneArea-textContainer"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="MuiTypography-root MuiDropzoneArea-text MuiTypography-h5"
|
|
||||||
>
|
|
||||||
Drag and drop a file here or click
|
|
||||||
</p>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiSvgIcon-root MuiDropzoneArea-icon"
|
|
||||||
focusable="false"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
|
|
||||||
tabindex="0"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="MuiButton-label"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="MuiTouchRipple-root"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
35
src/components/Home/index.jsx
Normal file
35
src/components/Home/index.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import useStyles from "../useStyles";
|
||||||
|
|
||||||
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { parsePayload } from "../../utils/jwt";
|
||||||
|
|
||||||
|
const DEFAULT_GREETING = "Welcome";
|
||||||
|
|
||||||
|
const getGreeting = (token) => {
|
||||||
|
if (!token || !token.idToken || !token.idToken.jwtToken)
|
||||||
|
return DEFAULT_GREETING;
|
||||||
|
|
||||||
|
const payload = parsePayload(token.idToken.jwtToken);
|
||||||
|
|
||||||
|
if (!payload || !payload.given_name) return DEFAULT_GREETING;
|
||||||
|
|
||||||
|
return `Welcome ${payload.given_name}!`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { token } = useUserContext();
|
||||||
|
const greeting = getGreeting(token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
{greeting}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@@ -20,7 +20,7 @@ export default function MenuDrawer({ children }) {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { signOut, token } = useUserContext();
|
const { signOut, token } = useUserContext();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(true);
|
||||||
|
|
||||||
const handleDrawerOpen = () => {
|
const handleDrawerOpen = () => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
@@ -30,6 +30,10 @@ export default function MenuDrawer({ children }) {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSignOut = () => {
|
||||||
|
document.location = signOut();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -59,7 +63,7 @@ export default function MenuDrawer({ children }) {
|
|||||||
{token !== null && (
|
{token !== null && (
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={signOut}
|
onClick={onSignOut}
|
||||||
className={classes.rightToolbar}
|
className={classes.rightToolbar}
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
@@ -96,7 +100,7 @@ export default function MenuDrawer({ children }) {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={classes.drawerHeader} />
|
<div className={classes.drawerHeader} />
|
||||||
<Container component="main" maxWidth="md">
|
<Container component="main" maxWidth="lg">
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,16 +1,50 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { List } from "@material-ui/core";
|
import { List } from "@material-ui/core";
|
||||||
import ListItemLink from "../ListItemLink";
|
import ListItemLink from "../ListItemLink";
|
||||||
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { Roles, hasRole } from "../../utils/roles";
|
||||||
|
|
||||||
|
const menuData = [
|
||||||
|
{
|
||||||
|
label: "Home",
|
||||||
|
to: "/home",
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "View Updates",
|
||||||
|
to: "/updates",
|
||||||
|
roles: [Roles.CREATE, Roles.READ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Create Updates",
|
||||||
|
to: "/package-upload",
|
||||||
|
roles: [Roles.CREATE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "View Vehicles",
|
||||||
|
to: "/vehicles",
|
||||||
|
roles: [Roles.CREATE, Roles.READ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Add Vehicles",
|
||||||
|
to: "/vehicle-add",
|
||||||
|
roles: [Roles.CREATE],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function SideMenu() {
|
export default function SideMenu() {
|
||||||
const menuData = [
|
const { groups } = useUserContext();
|
||||||
{ label: "Upload Update Package", to: "/home" },
|
const menu = menuData.reduce((result, item) => {
|
||||||
{ label: "Add Vehicles", to: "/vehicle-add" },
|
if (hasRole(item.roles, groups)) {
|
||||||
];
|
result.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
{menuData.map((item, index) => (
|
{menu.map((item, index) => (
|
||||||
<ListItemLink key={index} primary={item.label} to={item.to} />
|
<ListItemLink key={index} primary={item.label} to={item.to} />
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
33
src/components/Layouts/SideMenu.test.jsx
Normal file
33
src/components/Layouts/SideMenu.test.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
jest.mock("../Contexts/UserContext");
|
||||||
|
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { UserProvider, setToken } from "../Contexts/UserContext";
|
||||||
|
import { TEST_AUTH_OBJECT } from "../../utils/testing";
|
||||||
|
import SideMenu from "./SideMenu";
|
||||||
|
|
||||||
|
const renderMenu = async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<UserProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<SideMenu />
|
||||||
|
</BrowserRouter>
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {});
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("SideMenu", () => {
|
||||||
|
it("Unauthenticated", async () => {
|
||||||
|
setToken(null);
|
||||||
|
const container = await renderMenu(null);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Authenticated", async () => {
|
||||||
|
setToken(TEST_AUTH_OBJECT);
|
||||||
|
const container = await renderMenu(TEST_AUTH_OBJECT);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
159
src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap
Normal file
159
src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`SideMenu Authenticated 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-userprovider"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="MuiList-root MuiList-padding"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
|
href="/home"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiListItemText-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
|
href="/updates"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiListItemText-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
View Updates
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
|
href="/package-upload"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiListItemText-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
Create Updates
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
|
href="/vehicles"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiListItemText-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
View Vehicles
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
|
href="/vehicle-add"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiListItemText-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
Add Vehicles
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SideMenu Unauthenticated 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-userprovider"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="MuiList-root MuiList-padding"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||||
|
href="/home"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiListItemText-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Redirect, Route } from "react-router-dom";
|
import { Redirect, Route } from "react-router-dom";
|
||||||
|
import { hasRole } from "../../utils/roles";
|
||||||
|
|
||||||
export const TYPES = {
|
export const TYPES = {
|
||||||
PUBLIC: 0,
|
PUBLIC: 0,
|
||||||
@@ -7,11 +8,19 @@ export const TYPES = {
|
|||||||
PROTECTED: 2,
|
PROTECTED: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthRoute = ({ token, type, ...others }) => {
|
export const AuthRoute = ({ token, type, roles, groups, ...others }) => {
|
||||||
if (!token && type === TYPES.PROTECTED) {
|
if (type === TYPES.PROTECTED && !token) {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
} else if (token && type === TYPES.GUEST) {
|
} else if (type === TYPES.GUEST && token) {
|
||||||
|
return <Redirect to="/home" />;
|
||||||
|
} else if (
|
||||||
|
type === TYPES.PROTECTED &&
|
||||||
|
token &&
|
||||||
|
roles &&
|
||||||
|
!hasRole(roles, groups)
|
||||||
|
) {
|
||||||
return <Redirect to="/home" />;
|
return <Redirect to="/home" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Route render {...others} />;
|
return <Route render {...others} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Switch } from "react-router-dom";
|
|||||||
import { AuthRoute, TYPES } from "../Routes/AuthRoute";
|
import { AuthRoute, TYPES } from "../Routes/AuthRoute";
|
||||||
import { MessageBar } from "../MessageBar";
|
import { MessageBar } from "../MessageBar";
|
||||||
import { useUserContext } from "../Contexts/UserContext";
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { Roles } from "../../utils/roles";
|
||||||
|
|
||||||
const SSOForm = React.lazy(() => import("../SSOForm"));
|
const SSOForm = React.lazy(() => import("../SSOForm"));
|
||||||
const FileUploadForm = React.lazy(() => import("../FileUploadForm"));
|
const Home = React.lazy(() => import("../Home"));
|
||||||
const VehicleAddForm = React.lazy(() => import("../VehicleAddForm"));
|
const FileUploadForm = React.lazy(() => import("../UpdatePackages/Create"));
|
||||||
|
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
|
||||||
const PageNotFound = React.lazy(() => import("../404"));
|
const PageNotFound = React.lazy(() => import("../404"));
|
||||||
|
const UpdatePackagesForm = React.lazy(() => import("../UpdatePackages/List"));
|
||||||
|
const UpdatePackageEdit = React.lazy(() => import("../UpdatePackages/Edit"));
|
||||||
|
const CarUpdatesDeploy = React.lazy(() => import("../CarUpdates/Deploy"));
|
||||||
|
const CarUpdatesStatus = React.lazy(() => import("../CarUpdates/Status"));
|
||||||
|
const VehiclesList = React.lazy(() => import("../Cars/List"));
|
||||||
|
|
||||||
const SiteRoutes = () => {
|
const SiteRoutes = () => {
|
||||||
const { token } = useUserContext();
|
const { token, groups } = useUserContext();
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={"Loading..."}>
|
<Suspense fallback={"Loading..."}>
|
||||||
<MessageBar />
|
<MessageBar />
|
||||||
@@ -25,15 +32,65 @@ const SiteRoutes = () => {
|
|||||||
/>
|
/>
|
||||||
<AuthRoute
|
<AuthRoute
|
||||||
path="/home"
|
path="/home"
|
||||||
|
render={() => <Home />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/package-upload"
|
||||||
render={() => <FileUploadForm />}
|
render={() => <FileUploadForm />}
|
||||||
type={TYPES.PROTECTED}
|
type={TYPES.PROTECTED}
|
||||||
token={token}
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CREATE]}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/updates"
|
||||||
|
render={() => <UpdatePackagesForm />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CREATE]}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/update/:id"
|
||||||
|
render={() => <UpdatePackageEdit />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CREATE]}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/carupdate-deploy/:packageid"
|
||||||
|
render={() => <CarUpdatesDeploy />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CREATE]}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/carupdate-status/:packageid"
|
||||||
|
render={() => <CarUpdatesStatus />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.READ, Roles.CREATE]}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/vehicles"
|
||||||
|
render={() => <VehiclesList />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.READ, Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
<AuthRoute
|
<AuthRoute
|
||||||
path="/vehicle-add"
|
path="/vehicle-add"
|
||||||
render={() => <VehicleAddForm />}
|
render={() => <VehicleAddForm />}
|
||||||
type={TYPES.PROTECTED}
|
type={TYPES.PROTECTED}
|
||||||
token={token}
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
<PageNotFound />
|
<PageNotFound />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
jest.mock("../Contexts/UserContext");
|
jest.mock("../../Contexts/UserContext");
|
||||||
jest.mock("../Contexts/FileUploadContext");
|
jest.mock("../../Contexts/FileUploadContext");
|
||||||
jest.mock("../Contexts/VehicleContext");
|
jest.mock("../../Contexts/VehicleContext");
|
||||||
|
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { render, cleanup, waitFor } from "@testing-library/react";
|
import { render, cleanup, waitFor } from "@testing-library/react";
|
||||||
import FileUploadForm from "./index";
|
import FileUploadForm from "./index";
|
||||||
import { setToken } from "../Contexts/UserContext";
|
import { setToken } from "../../Contexts/UserContext";
|
||||||
import { StatusProvider } from "../Contexts/StatusContext";
|
import { StatusProvider } from "../../Contexts/StatusContext";
|
||||||
|
import { TEST_AUTH_OBJECT } from "../../../utils/testing"
|
||||||
|
|
||||||
describe("File Upload Form", () => {
|
describe("File Upload Form", () => {
|
||||||
it("Should render", async () => {
|
it("Should render", async () => {
|
||||||
setToken({ idToken: { jwtToken: "TEST" } });
|
setToken(TEST_AUTH_OBJECT);
|
||||||
const { container } = render(<StatusProvider><BrowserRouter><FileUploadForm /></BrowserRouter></StatusProvider>);
|
const { container } = render(<StatusProvider><BrowserRouter><FileUploadForm /></BrowserRouter></StatusProvider>);
|
||||||
await waitFor(() => {});
|
await waitFor(() => {});
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`File Upload Form Should render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid="mocked-fileuploadprovider"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="makeStyles-paper-1"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="MuiTypography-root MuiTypography-h5"
|
||||||
|
>
|
||||||
|
Upload Update Package
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
action="{onSubmit}"
|
||||||
|
class="makeStyles-form-3"
|
||||||
|
novalidate=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
for="packagename"
|
||||||
|
id="packagename-label"
|
||||||
|
>
|
||||||
|
Package name
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="packagename"
|
||||||
|
maxlength="255"
|
||||||
|
name="packagename"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Package name
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
for="version"
|
||||||
|
id="version-label"
|
||||||
|
>
|
||||||
|
Version
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="version"
|
||||||
|
maxlength="255"
|
||||||
|
name="version"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Version
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||||
|
data-shrink="false"
|
||||||
|
for="description"
|
||||||
|
id="description-label"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-multiline MuiOutlinedInput-multiline"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMultiline MuiOutlinedInput-inputMultiline"
|
||||||
|
id="description"
|
||||||
|
maxlength="5120"
|
||||||
|
name="description"
|
||||||
|
placeholder="Package description"
|
||||||
|
rows="4"
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Description
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||||
|
data-shrink="false"
|
||||||
|
for="releasenotes"
|
||||||
|
id="releasenotes-label"
|
||||||
|
>
|
||||||
|
Release Notes URL
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="releasenotes"
|
||||||
|
maxlength="1024"
|
||||||
|
name="releasenotes"
|
||||||
|
placeholder="Release Notes URL"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Release Notes URL
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiDropzoneArea-root"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
accept=""
|
||||||
|
autocomplete="off"
|
||||||
|
style="display: none;"
|
||||||
|
tabindex="-1"
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="MuiDropzoneArea-textContainer"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="MuiTypography-root MuiDropzoneArea-text MuiTypography-h5"
|
||||||
|
>
|
||||||
|
Drag and drop a file here or click
|
||||||
|
</p>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root MuiDropzoneArea-icon"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||||
|
tabindex="0"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiButton-label"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,27 +1,14 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useRef } from "react";
|
||||||
import {
|
import { Button, TextField, Typography } from "@material-ui/core";
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
useTheme,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { DropzoneArea } from "material-ui-dropzone";
|
import { DropzoneArea } from "material-ui-dropzone";
|
||||||
import { useUserContext } from "../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import { useStatusContext } from "../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext";
|
|
||||||
import {
|
import {
|
||||||
useFileUploadContext,
|
useFileUploadContext,
|
||||||
FileUploadProvider,
|
FileUploadProvider,
|
||||||
} from "../Contexts/FileUploadContext";
|
} from "../../Contexts/FileUploadContext";
|
||||||
import ModalProgressBar from "../ModalProgressBar";
|
import ModalProgressBar from "../../ModalProgressBar";
|
||||||
import useStyles from "../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
import menuItemStyle from "../menuItemStyle";
|
|
||||||
|
|
||||||
const FileUploadZone = ({ classes, token }) => {
|
const FileUploadZone = ({ classes, token }) => {
|
||||||
const { setFiles } = useFileUploadContext();
|
const { setFiles } = useFileUploadContext();
|
||||||
@@ -54,18 +41,12 @@ const FileUploadZone = ({ classes, token }) => {
|
|||||||
const MainForm = () => {
|
const MainForm = () => {
|
||||||
const { uploading, upload, files } = useFileUploadContext();
|
const { uploading, upload, files } = useFileUploadContext();
|
||||||
const { token } = useUserContext();
|
const { token } = useUserContext();
|
||||||
const { getVehicles, vehicles } = useVehicleContext();
|
|
||||||
const { setMessage } = useStatusContext();
|
const { setMessage } = useStatusContext();
|
||||||
const [selectedVehicles, setSelectedVehicles] = useState([]);
|
|
||||||
const theme = useTheme();
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const packagenameEl = useRef(null);
|
const packagenameEl = useRef(null);
|
||||||
const versionEl = useRef(null);
|
const versionEl = useRef(null);
|
||||||
const descEl = useRef(null);
|
const descEl = useRef(null);
|
||||||
const releasenotesEl = useRef(null);
|
const releasenotesEl = useRef(null);
|
||||||
const handleVehiclesChange = (event) => {
|
|
||||||
setSelectedVehicles(event.target.value);
|
|
||||||
};
|
|
||||||
const onSubmit = async (event) => {
|
const onSubmit = async (event) => {
|
||||||
try {
|
try {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -77,7 +58,6 @@ const MainForm = () => {
|
|||||||
version: versionEl.current.value,
|
version: versionEl.current.value,
|
||||||
description: descEl.current.value,
|
description: descEl.current.value,
|
||||||
releasenotes: releasenotesEl.current.value,
|
releasenotes: releasenotesEl.current.value,
|
||||||
vehicles: selectedVehicles,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await upload(formData, authToken, files);
|
await upload(formData, authToken, files);
|
||||||
@@ -86,20 +66,6 @@ const MainForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const {
|
|
||||||
idToken: { jwtToken: authToken },
|
|
||||||
} = token;
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await getVehicles(null, authToken);
|
|
||||||
} catch (e) {
|
|
||||||
setMessage(e.message);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.paper}>
|
<div className={classes.paper}>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
@@ -160,41 +126,6 @@ const MainForm = () => {
|
|||||||
placeholder="Release Notes URL"
|
placeholder="Release Notes URL"
|
||||||
inputRef={releasenotesEl}
|
inputRef={releasenotesEl}
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
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}
|
|
||||||
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>
|
|
||||||
<FileUploadZone classes={classes} />
|
<FileUploadZone classes={classes} />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -214,10 +145,8 @@ const MainForm = () => {
|
|||||||
|
|
||||||
export default function FileUploadForm() {
|
export default function FileUploadForm() {
|
||||||
return (
|
return (
|
||||||
<VehicleProvider>
|
<FileUploadProvider>
|
||||||
<FileUploadProvider>
|
<MainForm />
|
||||||
<MainForm />
|
</FileUploadProvider>
|
||||||
</FileUploadProvider>
|
|
||||||
</VehicleProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
198
src/components/UpdatePackages/Edit/index.jsx
Normal file
198
src/components/UpdatePackages/Edit/index.jsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { Button, TextField, Typography } from "@material-ui/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdatesProvider,
|
||||||
|
useUpdatesContext,
|
||||||
|
} from "../../Contexts/UpdatesContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
import { tsLocalDateTimeString } from "../../../utils/dates";
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { getPackages, updatePackage, packages, busy } = useUpdatesContext();
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
} = useUserContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const [packageName, setPackageName] = useState("");
|
||||||
|
const [version, setVersion] = useState("");
|
||||||
|
const [link, setLink] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [releaseNotesLink, setReleaseNotesLink] = useState("");
|
||||||
|
const [createDate, setCreateDate] = useState("");
|
||||||
|
const classes = useStyles();
|
||||||
|
const onSubmit = async (event) => {
|
||||||
|
try {
|
||||||
|
event.preventDefault();
|
||||||
|
const data = {
|
||||||
|
id: parseInt(id),
|
||||||
|
package_name: packageName,
|
||||||
|
version,
|
||||||
|
link,
|
||||||
|
desc: description,
|
||||||
|
release_notes: releaseNotesLink,
|
||||||
|
};
|
||||||
|
await updatePackage(data, token);
|
||||||
|
setMessage(`Updated ${packageName} ${version}`);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getData = async () => {
|
||||||
|
try {
|
||||||
|
getPackages({ id: parseInt(id) }, token);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleChange = (event) => {
|
||||||
|
const field = event.target.id;
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
if (field === "packagename") {
|
||||||
|
setPackageName(value);
|
||||||
|
} else if (field === "version") {
|
||||||
|
setVersion(value);
|
||||||
|
} else if (field === "link") {
|
||||||
|
setLink(value);
|
||||||
|
} else if (field === "description") {
|
||||||
|
setDescription(value);
|
||||||
|
} else if (field === "releasenotes") {
|
||||||
|
setReleaseNotesLink(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [token]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!packages || packages.length === 0) return;
|
||||||
|
var data = packages[0];
|
||||||
|
|
||||||
|
setPackageName(data.package_name);
|
||||||
|
setVersion(data.version);
|
||||||
|
setLink(data.link);
|
||||||
|
setDescription(data.desc || "");
|
||||||
|
setReleaseNotesLink(data.release_notes || "");
|
||||||
|
setCreateDate(tsLocalDateTimeString(data.timestamp));
|
||||||
|
}, [packages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Update Package {id}
|
||||||
|
</Typography>
|
||||||
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
|
<TextField
|
||||||
|
label="Create Date"
|
||||||
|
variant="filled"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
maxLength: "1024",
|
||||||
|
}}
|
||||||
|
value={createDate}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="packagename"
|
||||||
|
name="packagename"
|
||||||
|
label="Package name"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "255",
|
||||||
|
}}
|
||||||
|
value={packageName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="version"
|
||||||
|
name="version"
|
||||||
|
label="Version"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "255",
|
||||||
|
}}
|
||||||
|
value={version}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="link"
|
||||||
|
name="link"
|
||||||
|
label="Package link"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "1024",
|
||||||
|
}}
|
||||||
|
value={link}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "5120",
|
||||||
|
}}
|
||||||
|
value={description}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
placeholder="Package description"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="releasenotes"
|
||||||
|
name="releasenotes"
|
||||||
|
label="Release Notes URL"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "1024",
|
||||||
|
}}
|
||||||
|
value={releaseNotesLink}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Release Notes URL"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={busy}
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
{busy ? "Updating..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdatePackageEditForm = () => (
|
||||||
|
<UpdatesProvider>
|
||||||
|
<MainForm />
|
||||||
|
</UpdatesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UpdatePackageEditForm;
|
||||||
156
src/components/UpdatePackages/List/index.jsx
Normal file
156
src/components/UpdatePackages/List/index.jsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TablePagination,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import EditIcon from "@material-ui/icons/Edit";
|
||||||
|
import SendIcon from "@material-ui/icons/Send";
|
||||||
|
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
import {
|
||||||
|
UpdatesProvider,
|
||||||
|
useUpdatesContext,
|
||||||
|
} from "../../Contexts/UpdatesContext";
|
||||||
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
|
import { tsLocalDateTimeString } from "../../../utils/dates";
|
||||||
|
import { Roles, hasRole } from "../../../utils/roles";
|
||||||
|
|
||||||
|
const UpdatePackagesList = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [pageSize, setPageSize] = useState(5);
|
||||||
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
const { getPackages, packages, totalPackages } = useUpdatesContext();
|
||||||
|
const {
|
||||||
|
token: {
|
||||||
|
idToken: { jwtToken: token },
|
||||||
|
},
|
||||||
|
groups,
|
||||||
|
} = useUserContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPackages({ limit: pageSize, offset: pageSize * pageIndex }, token);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [pageIndex, pageSize, token]);
|
||||||
|
|
||||||
|
const handleChangePageIndex = (event, newIndex) => {
|
||||||
|
setPageIndex(newIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePageSize = (event) => {
|
||||||
|
setPageSize(parseInt(event.target.value, 10));
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Actions = (row) => {
|
||||||
|
let actions = [];
|
||||||
|
if (hasRole([Roles.CREATE, Roles.READ], groups)) {
|
||||||
|
actions.push({
|
||||||
|
tip: `Status "${row.package_name} ${row.version}"`,
|
||||||
|
link: `/carupdate-status/${row.id}`,
|
||||||
|
icon: (
|
||||||
|
<VisibilityIcon
|
||||||
|
aria-label={`Status ${row.package_name} ${row.version}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (hasRole([Roles.CREATE], groups)) {
|
||||||
|
actions = actions.concat([
|
||||||
|
{
|
||||||
|
tip: `Edit "${row.package_name} ${row.version}"`,
|
||||||
|
link: `/update/${row.id}`,
|
||||||
|
icon: (
|
||||||
|
<EditIcon aria-label={`Edit ${row.package_name} ${row.version}`} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tip: `Deploy "${row.package_name} ${row.version}"`,
|
||||||
|
link: `/carupdate-deploy/${row.id}`,
|
||||||
|
icon: (
|
||||||
|
<SendIcon
|
||||||
|
aria-label={`Deploy ${row.package_name} ${row.version}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.length === 0) return "No actions";
|
||||||
|
|
||||||
|
return actions.map((action) => (
|
||||||
|
<Tooltip key={action.link} title={action.tip}>
|
||||||
|
<Link to={action.link} style={{ margin: 5 }}>
|
||||||
|
{action.icon}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper} style={{ height: 700, width: "100%" }}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Updates
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center">ID</TableCell>
|
||||||
|
<TableCell align="center">Name</TableCell>
|
||||||
|
<TableCell align="center">Version</TableCell>
|
||||||
|
<TableCell align="center">Created</TableCell>
|
||||||
|
<TableCell align="right">Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{packages.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
<TableCell align="center">{row.id}</TableCell>
|
||||||
|
<TableCell align="center">{row.package_name}</TableCell>
|
||||||
|
<TableCell align="center">{row.version}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{tsLocalDateTimeString(row.timestamp)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">{Actions(row)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
colSpan={5}
|
||||||
|
count={totalPackages}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
page={pageIndex}
|
||||||
|
SelectProps={{
|
||||||
|
inputProps: { "aria-label": "rows per page" },
|
||||||
|
native: true,
|
||||||
|
}}
|
||||||
|
onChangePage={handleChangePageIndex}
|
||||||
|
onChangeRowsPerPage={handleChangePageSize}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdatePackagesForm = () => (
|
||||||
|
<UpdatesProvider>
|
||||||
|
<UpdatePackagesList />
|
||||||
|
</UpdatesProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UpdatePackagesForm;
|
||||||
@@ -24,7 +24,7 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
formControl: {
|
formControl: {
|
||||||
margin: theme.spacing(1),
|
margin: theme.spacing(3, 0, 2),
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
|
|||||||
48
src/services/__mocks__/updates.js
Normal file
48
src/services/__mocks__/updates.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const updatesAPI = {
|
||||||
|
createCarUpdates: async (data, token) => {
|
||||||
|
if (!data.id) data.id = 0;
|
||||||
|
data.id++;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPackages: async (search, token) => {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
package_name: "Test",
|
||||||
|
version: "1.0",
|
||||||
|
link: "http://cloudfront.com/download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
package_name: "Test",
|
||||||
|
version: "1.1",
|
||||||
|
link: "http://cloudfront.com/download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
package_name: "Test",
|
||||||
|
version: "1.2",
|
||||||
|
link: "http://cloudfront.com/download"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePackage: async (data, token) => {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deployPackage: async (data, token) => {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getCarUpdates: async (filter, token) => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updatesAPI;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import delay from "../../utils/delay";
|
import delay from "../../utils/delay";
|
||||||
|
|
||||||
let uploadFileResponse = { data: { link: "CLOUDFRONT_URL" } };
|
let uploadFileResponse = { link: "CLOUDFRONT_URL" };
|
||||||
let uploadFileDelay = false;
|
let uploadFileDelay = false;
|
||||||
let issuedCancelToken = null;
|
let issuedCancelToken = null;
|
||||||
|
|
||||||
|
|||||||
42
src/services/updates.js
Normal file
42
src/services/updates.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { getAuthHeaderOptions, fetchRespHandler, addQueryParams } from "../utils/http";
|
||||||
|
|
||||||
|
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
|
||||||
|
const updatesAPI = {
|
||||||
|
createCarUpdates: async (data, token) => fetch(`${API_ENDPOINT}/carupdate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler),
|
||||||
|
|
||||||
|
getPackages: async (search, token) => {
|
||||||
|
var u = addQueryParams(`${API_ENDPOINT}/updates`, search);
|
||||||
|
return fetch(u, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePackage: async (update, token) => fetch(`${API_ENDPOINT}/update`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
body: JSON.stringify(update),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler),
|
||||||
|
|
||||||
|
|
||||||
|
getCarUpdates: async (search, token) => {
|
||||||
|
var u = addQueryParams(`${API_ENDPOINT}/carupdates`, search);
|
||||||
|
return fetch(u, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updatesAPI;
|
||||||
@@ -29,5 +29,15 @@ export const uploadFile = (file, data, token, onProgress, cancelToken) => {
|
|||||||
form.append(key, data[key]);
|
form.append(key, data[key]);
|
||||||
}
|
}
|
||||||
form.append("file", file);
|
form.append("file", file);
|
||||||
return axios.post(`${UPLOAD_ENDPOINT}/upload`, form, options);
|
return axios.post(`${UPLOAD_ENDPOINT}/update`, form, options)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.catch((error) => {
|
||||||
|
if (typeof error.response.data === "string") {
|
||||||
|
return {
|
||||||
|
error: error.response.statusText,
|
||||||
|
message: error.response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return error.response.data;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAuthHeaderOptions, fetchRespHandler } from "../utils/http"
|
import { getAuthHeaderOptions, fetchRespHandler, addQueryParams } from "../utils/http"
|
||||||
|
|
||||||
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
|
||||||
@@ -9,12 +9,16 @@ const vehiclesAPI = {
|
|||||||
body: JSON.stringify(vehicle),
|
body: JSON.stringify(vehicle),
|
||||||
})
|
})
|
||||||
.then(fetchRespHandler),
|
.then(fetchRespHandler),
|
||||||
getVehicles: async (search, token) => fetch(`${API_ENDPOINT}/vehicles`, {
|
|
||||||
method: "GET",
|
getVehicles: async (search, token) => {
|
||||||
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
const u = addQueryParams(`${API_ENDPOINT}/vehicles`, search);
|
||||||
|
return fetch(u, {
|
||||||
})
|
method: "GET",
|
||||||
.then(fetchRespHandler)
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default vehiclesAPI;
|
export default vehiclesAPI;
|
||||||
|
|||||||
13
src/utils/dates.js
Normal file
13
src/utils/dates.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const ts2DateTime = (timestamp) => {
|
||||||
|
return new Date(timestamp * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tsLocalDateTimeString = (timestamp) => {
|
||||||
|
const date = ts2DateTime(timestamp);
|
||||||
|
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocalDateTimeString = (datestring) => {
|
||||||
|
const date = new Date(datestring.replace(' ', 'T'));
|
||||||
|
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
|
||||||
|
}
|
||||||
@@ -11,4 +11,14 @@ export const fetchRespHandler = (response) => {
|
|||||||
error: response.statusText,
|
error: response.statusText,
|
||||||
message: `${response.status} ${response.statusText}`,
|
message: `${response.status} ${response.statusText}`,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const addQueryParams = (url, params) => {
|
||||||
|
if (!params) return url;
|
||||||
|
|
||||||
|
var u = new URL(url);
|
||||||
|
|
||||||
|
Object.keys(params).forEach(key => u.searchParams.append(key, params[key]))
|
||||||
|
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|||||||
42
src/utils/http.test.js
Normal file
42
src/utils/http.test.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { getAuthHeaderOptions, addQueryParams } from "./http";
|
||||||
|
|
||||||
|
describe("HTTP Helper", () => {
|
||||||
|
it("Authorization header", () => {
|
||||||
|
const result = getAuthHeaderOptions("TEST_TOKEN");
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result.Authorization).toBeTruthy();
|
||||||
|
expect(result.Authorization).toEqual("Bearer TEST_TOKEN");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addQueryParams", () => {
|
||||||
|
it("String and number params", () => {
|
||||||
|
const params = {
|
||||||
|
a: "a",
|
||||||
|
b: "b",
|
||||||
|
c: 1,
|
||||||
|
d: 3.14,
|
||||||
|
}
|
||||||
|
const result = addQueryParams("http://example.com", params);
|
||||||
|
|
||||||
|
expect(result).toEqual("http://example.com/?a=a&b=b&c=1&d=3.14");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Null params", () => {
|
||||||
|
const params = null;
|
||||||
|
const result = addQueryParams("http://example.com", params);
|
||||||
|
|
||||||
|
expect(result).toEqual("http://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Undefined params", () => {
|
||||||
|
const result = addQueryParams("http://example.com");
|
||||||
|
expect(result).toEqual("http://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Empty params", () => {
|
||||||
|
const result = addQueryParams("http://example.com", {});
|
||||||
|
expect(result).toEqual("http://example.com/");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
14
src/utils/jwt.js
Normal file
14
src/utils/jwt.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const parsePayload = (token) => {
|
||||||
|
if (!token) return null;
|
||||||
|
const parts = token.split(".");
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
return JSON.parse(decode(parts[1]));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decode = (payload) => {
|
||||||
|
const l = (payload.length % 4);
|
||||||
|
if (l > 0) {
|
||||||
|
payload += "=".repeat(4 - l);
|
||||||
|
}
|
||||||
|
return atob(payload);
|
||||||
|
};
|
||||||
14
src/utils/jwt.test.js
Normal file
14
src/utils/jwt.test.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { parsePayload } from "./jwt";
|
||||||
|
import { TEST_TOKEN } from "./testing";
|
||||||
|
|
||||||
|
describe("JWT Helper", () => {
|
||||||
|
it("Should decode", () => {
|
||||||
|
const start = Date.now()
|
||||||
|
const v = parsePayload(TEST_TOKEN);
|
||||||
|
const diff = Date.now() - start;
|
||||||
|
|
||||||
|
expect(diff < 2).toBeTruthy();
|
||||||
|
expect(typeof v).toEqual("object");
|
||||||
|
expect(v.exp).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
35
src/utils/roles.js
Normal file
35
src/utils/roles.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { parsePayload } from "./jwt";
|
||||||
|
|
||||||
|
export const Roles = {
|
||||||
|
CREATE: "efcc3025-e2d8-4212-8227-805c7be39d2c",
|
||||||
|
READ: "a729bbd4-2038-4649-9127-16782bb1e701",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasRoleToken = (roles, token) => {
|
||||||
|
if (!roles || roles.length === 0) return true;
|
||||||
|
|
||||||
|
const groups = getGroups(token);
|
||||||
|
|
||||||
|
if (!groups) return false;
|
||||||
|
|
||||||
|
return hasRole(roles, groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGroups = (token) => {
|
||||||
|
const payload = parsePayload(token);
|
||||||
|
|
||||||
|
if (!payload || !payload["custom:groups"]) return null;
|
||||||
|
|
||||||
|
return payload["custom:groups"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasRole = (roles, groups) => {
|
||||||
|
if (!roles || roles.length === 0) return true;
|
||||||
|
if (!groups) return false;
|
||||||
|
|
||||||
|
for (let role of roles) {
|
||||||
|
if (groups.indexOf(role) > -1) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
10
src/utils/roles.test.js
Normal file
10
src/utils/roles.test.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
import { hasRoleToken, Roles } from "./roles";
|
||||||
|
import { TEST_TOKEN } from "./testing";
|
||||||
|
|
||||||
|
describe("Roles Helper", () => {
|
||||||
|
it("Check roles", () => {
|
||||||
|
expect(hasRoleToken([Roles.CREATE], TEST_TOKEN)).toEqual(true);
|
||||||
|
expect(hasRoleToken([Roles.READ], TEST_TOKEN)).toEqual(false);
|
||||||
|
})
|
||||||
|
});
|
||||||
8
src/utils/testing.js
Normal file
8
src/utils/testing.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const TEST_TOKEN = "eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiOUlyV2RLaUxJU0FZUnFha1F2b2xmZyIsInN1YiI6IjJiMDk1NTY2LTllNDYtNGQ4ZS1iMTA5LTI0MTM1ZGYyMmVlNiIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJjdXN0b206Z3JvdXBzIjoiWzhkODI3OGE1LTljMGUtNGM3Zi05MThhLTgxMWZkMWQyMzZlNCwgNmMzY2Y5OGQtMGFkYS00OGM2LWFlOTQtYjE3MWNmYTI3NWZjLCA1NmVmNGJlYy1kNzM5LTRkZGYtYTAwMy1lY2M4MTMwODViOGQsIGVmY2MzMDI1LWUyZDgtNDIxMi04MjI3LTgwNWM3YmUzOWQyYywgNTUxNWE5OGYtNDY2OC00MTIxLThlOGQtZmVlMjgyNTY5OWNmLCA4Njk1NmEyZi04ZDQ2LTQ3ZmYtOWIyOS1mOTkwNzlhZTNjMWQsIGM0ZDQzNjFjLTg4ODItNDdiNC04NjQxLWZkM2FiNjhhZTcyMiwgN2JjZGNkYjItMzI3OS00NGJmLWE5OTgtNzcxYmFiNGIzM2UxXSIsImF1ZCI6IjdjazJ0Zm9xYXZjNzJjNDVoaDd0Z2U0MmtkIiwiY3VzdG9tOnNlc3Npb24tZHVyYXRpb24iOiI5MDAiLCJpZGVudGl0aWVzIjpbeyJ1c2VySWQiOiJqd3VAZmlza2VyaW5jLmNvbSIsInByb3ZpZGVyTmFtZSI6IkF6dXJlQUQiLCJwcm92aWRlclR5cGUiOiJTQU1MIiwiaXNzdWVyIjoiaHR0cHM6XC9cL3N0cy53aW5kb3dzLm5ldFwvNWFhNGI2NDAtYzlmYy00YTliLWIzYTMtZDRhN2QwMDhmYjVlXC8iLCJwcmltYXJ5IjoidHJ1ZSIsImRhdGVDcmVhdGVkIjoiMTYxNDM2NDk3NDU4NSJ9XSwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2MTU4MjEzMDksImV4cCI6MTYxNTkyNzM0OSwiaWF0IjoxNjE1OTIzNzUwLCJmYW1pbHlfbmFtZSI6Ild1IiwiZW1haWwiOiJqd3VAZmlza2VyaW5jLmNvbSJ9.R3k-YGK0MrUdW030Xj2WxM7mdsm1tlobeDq3YRMIKMtdkJsf5qjwM_wqVPbErH-8OrFLW7YIPuMo2Rh5PCGvg4I6kL-tWfDOY4o5b5r_VdiifXov0be_ukdt5pZblhgg0dYSLmFaFZsxNjEng8-obl_FnWp6VtG1lnRGwORY3pFe88W7OM3zLMC0g-otfAEQ2KSOaV9bfUoRAaZaGlHe8ooIQx8Qoer9qYsnymK0Sk7jSZKwhtFsziSarhreHmBkCLaWBHDjc9PQDtBvO8wg1KMKmM-6oewA0xTKPtsuHxnvtVANYaR7Nqp9cbF940YRf2IK5FB7KWFtcR7Y6igLXw";
|
||||||
|
export const TEST_AUTH_OBJECT = {
|
||||||
|
idToken: {
|
||||||
|
jwtToken: TEST_TOKEN,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const TEST_EXPECTED_GROUPS =
|
||||||
|
"[8d8278a5-9c0e-4c7f-918a-811fd1d236e4, 6c3cf98d-0ada-48c6-ae94-b171cfa275fc, 56ef4bec-d739-4ddf-a003-ecc813085b8d, efcc3025-e2d8-4212-8227-805c7be39d2c, 5515a98f-4668-4121-8e8d-fee2825699cf, 86956a2f-8d46-47ff-9b29-f99079ae3c1d, c4d4361c-8882-47b4-8641-fd3ab68ae722, 7bcdcdb2-3279-44bf-a998-771bab4b33e1]";
|
||||||
Reference in New Issue
Block a user