CEC-749 Generate cert UI (#141)

* Add Create Certificate page

* Tests

* Update permission check

* Use Azure
This commit is contained in:
John Wu
2022-04-18 16:50:51 -07:00
committed by GitHub
parent 81aeedc521
commit 56bef0c34d
28 changed files with 2449 additions and 289 deletions

View File

@@ -37,22 +37,35 @@ const check = async (path, selector, compare) => {
const sleepAndCheck = async (path, selector, compare) => {
const container = await renderRoute(path);
await waitFor(() => { });
await waitFor(() => {});
expect(container.querySelector(selector).innerHTML).toEqual(compare);
expect(container).toMatchSnapshot();
};
describe("App", () => {
const rxMakeStyles = /makeStyles-(\w+)-(\d+)/gi;
beforeAll(() => {
// Stablize Table Pagination control ids
expect.addSnapshotSerializer({
test: function (val) {
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
return val && typeof val === "string" && val.indexOf("mui-") > -1;
},
print: function (val) {
let str = val;
str = str.replace(/mui-\d*/g, "mui-00000");
return `"${str}"`;
},
});
expect.addSnapshotSerializer({
test: (val) => {
return val && typeof val === "string" && val.search(rxMakeStyles) > -1;
},
print: function (val) {
let str = val;
str = str.replace(rxMakeStyles, "makeStyles-$1-0000");
return `"${str}"`;
},
});
@@ -107,6 +120,10 @@ describe("App", () => {
);
});
it("Route /tools/certificates/add unauthenticated", async () => {
await check("/tools/certificates/add", "span.MuiButton-label", "Sign In");
});
it("Route /page-not-found unauthenticated", async () => {
await check("/page-not-found", "h1", "Page Not Found");
});
@@ -159,4 +176,9 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
});
it("Route /tools/certificates/add authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/tools/certificates/add", "h6", "Create Certificate");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -194,6 +194,23 @@ exports[`CANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -230,6 +247,23 @@ exports[`CANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Delete 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -266,6 +300,23 @@ exports[`CANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -193,6 +193,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -229,6 +246,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Delete 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -265,6 +299,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -0,0 +1,90 @@
import React, { useRef, useState } from "react";
import {
Button,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
TextField,
} from "@material-ui/core";
import useStyles from "../../useStyles";
import { CertTypes } from "../../Contexts/CertificateContext";
const CreateForm = ({ onCreate, busy }) => {
const classes = useStyles();
const vinEl = useRef(null);
const [certType, setCertType] = useState(CertTypes.TREX);
const onSubmit = async (event) => {
event.preventDefault();
if (onCreate)
onCreate({
vin: vinEl.current.value,
type: certType,
});
};
const onCertTypeChange = (event) => {
setCertType(event.target.value);
};
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"
name="vin"
label="VIN"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "17",
}}
required
fullWidth
inputRef={vinEl}
/>
<FormLabel id="cert-type-group-label">Type</FormLabel>
<RadioGroup
row
aria-labelledby="cert-type-group-label"
name="cert-type"
value={certType}
onChange={onCertTypeChange}
margin="normal"
>
<FormControlLabel
value={CertTypes.TREX}
control={<Radio />}
label="TREX"
/>
<FormControlLabel
value={CertTypes.HMI}
control={<Radio />}
label="HMI"
/>
<FormControlLabel
value={CertTypes.Charging}
control={<Radio />}
label="Charging"
/>
</RadioGroup>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{busy ? "Submitting..." : "Submit"}
</Button>
</form>
</div>
);
};
export default CreateForm;

View File

@@ -0,0 +1,50 @@
import { Button } from "@material-ui/core";
import React from "react";
import DownloadFileLink from "../../Controls/DownloadFileLink";
import useStyles from "../../useStyles";
const CertMimeType = "application/x-pem-file";
const DownloadCerts = ({ vin, publicCert, privateCert, onChangeView }) => {
const classes = useStyles();
const onNewCert = (event) => {
event.preventDefault();
if (!onChangeView) return;
onChangeView();
};
return (
<div>
<h2>Download Certifcates</h2>
<ul>
<li>
<DownloadFileLink
data={publicCert}
filename={`${vin}_cert.pem`}
mimetype={CertMimeType}
/>
</li>
<li>
<DownloadFileLink
data={privateCert}
filename={`${vin}_key.pem`}
mimetype={CertMimeType}
/>
</li>
</ul>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onNewCert}
>
Done
</Button>
</div>
);
};
export default DownloadCerts;

View File

@@ -0,0 +1,25 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import DownloadCerts from "./DownloadCerts";
describe("DownloadCerts", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<DownloadCerts
vin={"TESTVIN"}
publicCert={"PUBLIC"}
privateCert={"PRIVATE"}
/>
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownloadCerts Render 1`] = `
<div>
<div>
<h2>
Download Certifcates
</h2>
<ul>
<li>
<a
download="TESTVIN_cert.pem"
>
TESTVIN_cert.pem
</a>
</li>
<li>
<a
download="TESTVIN_key.pem"
>
TESTVIN_key.pem
</a>
</li>
</ul>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Done
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
`;

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useState } from "react";
import {
useCertificateContext,
CertificateProvider,
} from "../../Contexts/CertificateContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
import CreateForm from "./CreateForm";
import DownloadCerts from "./DownloadCerts";
const VIEW_FORM = 0;
const VIEW_DOWNLOAD = 1;
const MainForm = () => {
const { busy, createCert } = useCertificateContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [view, setView] = useState(VIEW_FORM);
const [pubCert, setPubCert] = useState(null);
const [privCert, setPrivCert] = useState(null);
const [vin, setVIN] = useState(null);
useEffect(() => {
setTitle("Create Certificate");
setSitePath([
{
label: "Tools",
link: "/tools/certificates/add",
},
{
label: "Create Certificate",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onCreate = async (data) => {
try {
const result = await createCert(data, token);
setPubCert(result.public_key);
setPrivCert(result.private_key);
setVIN(data.vin);
setMessage(`Created ${data.vin} certificate`);
setView(VIEW_DOWNLOAD);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const onChangeView = () => {
setPubCert(null);
setPrivCert(null);
setVIN(null);
setView(VIEW_FORM);
};
if (view === VIEW_DOWNLOAD)
return (
<DownloadCerts
vin={vin}
publicCert={pubCert}
privateCert={privCert}
onChangeView={onChangeView}
/>
);
return <CreateForm onCreate={onCreate} busy={busy} />;
};
const CertificateCreate = () => (
<CertificateProvider>
<MainForm />
</CertificateProvider>
);
export default CertificateCreate;

View File

@@ -0,0 +1,50 @@
import React, { useContext, useState } from "react";
import api from "../../services/certificatesAPI";
const CertificateContext = React.createContext();
export const CertTypes = {
TREX: "TREX",
HMI: "HMI",
Charging: "CHARGE",
};
const validateCreate = (data) => {
if (!data.type) throw new Error("type is required");
if (!data.vin) throw new Error("vin is required");
};
export const CertificateProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const createCert = async (data, token) => {
try {
setBusy(true);
validateCreate(data);
const result = await api.create(data, token);
if (result.error) {
throw new Error(`Create certificate error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
return (
<CertificateContext.Provider
value={{
busy,
createCert,
}}
>
{children}
</CertificateContext.Provider>
);
};
export const useCertificateContext = () => useContext(CertificateContext);

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownloadFileLink Render 1`] = `
<div>
<a
download="test.txt"
>
test.txt
</a>
</div>
`;

View File

@@ -0,0 +1,32 @@
import React, { useEffect, useState } from "react";
const DownloadFileLink = ({ data, filename, mimetype }) => {
const [link, setLink] = useState("");
const releaseLink = () => {
if (link === "") return;
URL.revokeObjectURL(link);
};
const makeFile = () => {
const file = new Blob([data], { type: mimetype ?? "text/plain" });
releaseLink();
setLink(URL.createObjectURL(file));
};
useEffect(() => {
if (!data) return;
makeFile();
return releaseLink;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, filename, mimetype]);
return (
<a download={filename ?? "file.txt"} href={link}>
{filename}
</a>
);
};
export default DownloadFileLink;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import DownloadFileLink from ".";
describe("DownloadFileLink", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<DownloadFileLink data={"ABCDEFGHIJK"} filename="test.txt" />
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -195,6 +195,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123-456\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123-456"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -231,6 +248,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -267,6 +301,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1000\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1000"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -153,7 +153,23 @@ exports[`FleetVehiclesTable Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345678\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345678"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -171,7 +187,23 @@ exports[`FleetVehiclesTable Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345679\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345679"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -189,7 +221,23 @@ exports[`FleetVehiclesTable Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345670\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345670"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -194,6 +194,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123-456\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123-456"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -230,6 +247,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -266,6 +300,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1000\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1000"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -152,7 +152,23 @@ exports[`VehiclesTab Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345678\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345678"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -170,7 +186,23 @@ exports[`VehiclesTab Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345679\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345679"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -188,7 +220,23 @@ exports[`VehiclesTab Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345670\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345670"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -1,10 +1,11 @@
import React from "react";
import { List } from "@material-ui/core";
import HomeIcon from "@material-ui/icons/Home";
import DirectionsCarIcon from '@material-ui/icons/DirectionsCar';
import DirectionsCarIcon from "@material-ui/icons/DirectionsCar";
import CommuteIcon from "@material-ui/icons/Commute";
import CloudDownloadIcon from "@material-ui/icons/CloudDownload";
import AssessmentIcon from "@material-ui/icons/Assessment";
import BuildIcon from "@material-ui/icons/Build";
import ListItemLink from "../ListItemLink";
import ListItemExternalLink from "../ListItemExternalLink";
@@ -43,6 +44,19 @@ const menuData = [
icon: <AssessmentIcon />,
roles: [Roles.READ, Roles.CREATE],
},
{
label: "Tools",
to: "/tools/certificates/add",
icon: <BuildIcon />,
roles: [Roles.CERTIFICATES],
submenus: [
{
label: "Certificate",
to: "/tools/certificates/add",
roles: [Roles.CERTIFICATES],
},
],
},
];
const MenuItem = ({ item, children }) => {

View File

@@ -190,6 +190,70 @@ exports[`SideMenu Authenticated 1`] = `
/>
</a>
</li>
<span>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/certificates/add"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Tools
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</span>
<ul
style="margin-left: 50px;"
>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/certificates/add"
role="button"
tabindex="0"
>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Certificate
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</ul>
</ul>
</div>
</div>

View File

@@ -6,8 +6,8 @@ import { MessageBar } from "../MessageBar";
import { useUserContext } from "../Contexts/UserContext";
import { Roles } from "../../utils/roles";
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"))
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"))
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"));
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"));
const CarsList = React.lazy(() => import("../Cars/List"));
const CarStatus = React.lazy(() => import("../Cars/Status"));
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
@@ -15,9 +15,15 @@ const FleetsList = React.lazy(() => import("../Fleets/Table"));
const FleetStatus = React.lazy(() => import("../Fleets/Status"));
const FleetAddForm = React.lazy(() => import("../Fleets/Add"));
const FleetUpdateForm = React.lazy(() => import("../Fleets/Update"));
const FleetAddVehicleForm = React.lazy(() => import("../Fleets/Status/Vehicles/Add"));
const FleetAddCANFilterForm = React.lazy(() => import("../Fleets/Status/CANFilters/Add"));
const FleetUpdateCANFilterForm = React.lazy(() => import("../Fleets/Status/CANFilters/Update"));
const FleetAddVehicleForm = React.lazy(() =>
import("../Fleets/Status/Vehicles/Add")
);
const FleetAddCANFilterForm = React.lazy(() =>
import("../Fleets/Status/CANFilters/Add")
);
const FleetUpdateCANFilterForm = React.lazy(() =>
import("../Fleets/Status/CANFilters/Update")
);
const Home = React.lazy(() => import("../Home"));
const Manifests = React.lazy(() => import("../Manifest/List"));
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
@@ -26,7 +32,8 @@ const ManifestCreate = React.lazy(() => import("../Manifest/Create"));
const PageNotFound = React.lazy(() => import("../404"));
const SSOForm = React.lazy(() => import("../SSOForm"));
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
const VehicleUpdateForm = React.lazy(() => import("../Cars/Update"))
const VehicleUpdateForm = React.lazy(() => import("../Cars/Update"));
const CertificateCreate = React.lazy(() => import("../Certificates/Add"));
const SiteRoutes = () => {
const { token, groups } = useUserContext();
@@ -191,6 +198,14 @@ const SiteRoutes = () => {
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/tools/certificates/add"
render={() => <CertificateCreate />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CERTIFICATES]}
/>
<PageNotFound />
</Switch>
</Suspense>