diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index cb1b82f..02bd570 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -4395,48 +4395,6 @@ exports[`App Route /package-deploy authenticated 1`] = `
-
- -
- + +
+
+ + +
+ + + + + +`; diff --git a/src/components/SUMS/Add/index.jsx b/src/components/SUMS/Add/index.jsx new file mode 100644 index 0000000..4ef751b --- /dev/null +++ b/src/components/SUMS/Add/index.jsx @@ -0,0 +1,116 @@ +import { + Button, + TextField +} from "@material-ui/core"; +import { useParams } from "react-router"; +import React, { useEffect, useState } from "react"; +import { Redirect } from "react-router"; +import { logger } from "../../../services/monitoring"; +import { useStatusContext } from "../../Contexts/StatusContext"; +import { + CarUpdatesProvider, + useCarUpdatesContext +} from "../../Contexts/CarUpdatesContext"; +import { useUserContext } from "../../Contexts/UserContext"; +import useStyles from "../../useStyles"; + +const MainForm = () => { + const { addSUMSVersionRxSwins, busy } = useCarUpdatesContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + } = useUserContext(); + const classes = useStyles(); + const [redirect, setRedirect] = useState(null); + const { sums_version } = useParams(); + const { setMessage, setTitle, setSitePath } = useStatusContext(); + const [rxswin, setRxswin] = useState(""); + + useEffect(() => { + setTitle(`Add RXSWIN to SUMS Version ${sums_version}`); + setSitePath([ + { + label: `SUMS Version ${sums_version}`, + link: `/sums/${sums_version}`, + }, + { + label: `Add RXSWIN`, + }, + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onRxswinChange = (event) => { + setRxswin(event.target.value); + } + + const onSubmit = async (event) => { + try { + event.preventDefault(); + + const data = { + "swversion_rxswins": [ + { + "version": sums_version, + "rxswin": rxswin, + }, + ], + }; + + const result = await addSUMSVersionRxSwins(sums_version, data, token); + if (!result || result.error) return; + + setMessage(`Added ${rxswin}`); + setRedirect(`/sums/${sums_version}`); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; + + if (redirect && redirect.length > 0) { + return ; + } + + return ( +
+
+ + + +
+ ); +}; + +const SumsRxSwinAdd = () => ( + + + +); + +export default SumsRxSwinAdd; \ No newline at end of file diff --git a/src/components/SUMS/Add/index.test.jsx b/src/components/SUMS/Add/index.test.jsx new file mode 100644 index 0000000..f44513f --- /dev/null +++ b/src/components/SUMS/Add/index.test.jsx @@ -0,0 +1,46 @@ +jest.mock("../../Contexts/CarUpdatesContext"); +jest.mock("../../Contexts/StatusContext"); +jest.mock("../../Contexts/UserContext"); +jest.mock("@material-ui/core/utils/unstable_useId", () => + jest.fn().mockReturnValue("mui-test-id") +); +import { render, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route } from "react-router-dom"; +import { CarUpdatesProvider } from "../../Contexts/CarUpdatesContext"; +import { StatusProvider } from "../../Contexts/StatusContext"; +import { UserProvider, setToken } from "../../Contexts/UserContext"; +import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; +import MainForm from "./index"; +import addSnapshotSerializer from "../../../utils/snapshot"; + +const renderSUMSAdd = async () => { + const { container } = render( + + + + + + + + + + + + ); + await waitFor(() => { + /* render */ + }); + return container; +}; + +describe("SUMSAdd", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + setToken(TEST_AUTH_OBJECT_FISKER); + const container = await renderSUMSAdd(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/SUMS/__snapshots__/index.test.jsx.snap b/src/components/SUMS/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..ee5e68c --- /dev/null +++ b/src/components/SUMS/__snapshots__/index.test.jsx.snap @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SUMS Render 1`] = ` +
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + +
+ + RXSWIN (RX Software Identification Number) + + + + + Created + + sorted descending + + + +
+
+
+
+
+
+
+
+`; diff --git a/src/components/SUMS/index.jsx b/src/components/SUMS/index.jsx new file mode 100644 index 0000000..82e64be --- /dev/null +++ b/src/components/SUMS/index.jsx @@ -0,0 +1,233 @@ +import { + Grid, + IconButton, + Table, + TableBody, + TableCell, + TableFooter, + TablePagination, + TableRow, +} from "@material-ui/core"; +import AddCircleIcon from "@material-ui/icons/AddCircle"; +import DeleteIcon from "@material-ui/icons/Delete"; +import clsx from "clsx"; +import { LocalDateTimeString } from "../../utils/dates"; +import { logger } from "../../services/monitoring"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { + CarUpdatesProvider, + useCarUpdatesContext +} from "../Contexts/CarUpdatesContext"; +import { useStatusContext } from "../Contexts/StatusContext"; +import { useUserContext } from "../Contexts/UserContext"; +import { Link } from "react-router-dom"; +import TableHeaderSortable from "../Table/HeaderSortable"; +import { useLocalStorage } from "../useLocalStorage"; +import DeleteConfirmation from "../DeleteConfirmation"; +import useStyles from "../useStyles"; +import { Permissions } from "../../utils/roles"; +import { RoleWrap } from "../Controls/RoleWrap"; + +const tableColumns = [ + { + id: "rxswin", + label: "RXSWIN (RX Software Identification Number)", + }, + { + id: "created_at", + label: "Created", + }, +]; + +const PAGE_SIZE = "SUMS_RXSWIN_TABLE_PAGE_SIZE"; + +const MainForm = () => { + const { sums_version } = useParams(); + const classes = useStyles(); + const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); + const [pageIndex, setPageIndex] = useState(0); + const [orderBy, setOrderBy] = useState("created_at"); + const [order, setOrder] = useState("desc"); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [rowToDelete, setRowToDelete] = useState({}); + const { + deleteSUMSVersionRxSwins, + getSUMSVersionRxSwins, + versionRxSwins, + totalVersionRxSwins, + stopMonitor, + } = useCarUpdatesContext(); + const { + token: { + idToken: { jwtToken: token }, + }, + groups, + providers, + } = useUserContext(); + const { setMessage, setTitle, setSitePath } = useStatusContext(); + + useEffect(() => { + setTitle(`SUMS Version ${sums_version}`); + setSitePath([ + { + label: `SUMS Version ${sums_version}`, + link: `/sums/${sums_version}`, + }, + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + loadSUMSVersionRxSwins(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sums_version, token, pageIndex, pageSize, orderBy, order]); + + const loadSUMSVersionRxSwins = async () => { + try { + if (!sums_version || !token) return; + stopMonitor(); + await getSUMSVersionRxSwins( + sums_version, + { + limit: pageSize, + offset: pageSize * pageIndex, + order: `${orderBy} ${order}`, + }, + token + ); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + }; + + const handleChangePageIndex = (event, newIndex) => { + setPageIndex(newIndex); + }; + + const handleChangePageSize = (event) => { + setPageSize(parseInt(event.target.value, 10)); + setPageIndex(0); + }; + + const handleSort = (event, property) => { + try { + if (property === orderBy) { + if (order === "asc") { + setOrder("desc"); + } else { + setOrder("asc"); + } + } else { + setOrderBy(property); + setOrder("asc"); + } + } catch (e) { + logger.warn(e.stack); + } + }; + + const onDeleteClick = (row) => { + setRowToDelete(row); + setShowDeleteModal(true); + } + + const sendDelete = async () => { + if (rowToDelete) { + try { + const result = await deleteSUMSVersionRxSwins(rowToDelete.version, rowToDelete.rxswin, token); + if (!result || result.error) return; + + setMessage(`Deleted ${rowToDelete.rxswin}`); + loadSUMSVersionRxSwins(); + } catch (e) { + setMessage(e.message); + logger.warn(e.stack); + } + } + }; + + return ( +
+ + + + + + + + + + + {versionRxSwins && versionRxSwins.map((row, index) => ( + + + {row.rxswin} + + + {LocalDateTimeString(row.created)} + + + + onDeleteClick(row)} + aria-label={`Send delete for ${row.rxswin}`} + size="small" + color="primary" + > + + + + + + ))} + + + + {!versionRxSwins || versionRxSwins.length === 0 ? ( + No RX Software Identification Numbers + ) : ( + )} + + +
+ setShowDeleteModal(false)} + deleteFunction={sendDelete} + /> +
+ ); +}; + +const SumsRxSwin = () => ( + + + +); + +export default SumsRxSwin; \ No newline at end of file diff --git a/src/components/SUMS/index.test.jsx b/src/components/SUMS/index.test.jsx new file mode 100644 index 0000000..cfd5390 --- /dev/null +++ b/src/components/SUMS/index.test.jsx @@ -0,0 +1,46 @@ +jest.mock("../Contexts/CarUpdatesContext"); +jest.mock("../Contexts/StatusContext"); +jest.mock("../Contexts/UserContext"); +jest.mock("@material-ui/core/utils/unstable_useId", () => + jest.fn().mockReturnValue("mui-test-id") +); +import { render, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route } from "react-router-dom"; +import { CarUpdatesProvider } from "../Contexts/CarUpdatesContext"; +import { StatusProvider } from "../Contexts/StatusContext"; +import { UserProvider, setToken } from "../Contexts/UserContext"; +import { TEST_AUTH_OBJECT_FISKER } from "../../utils/testing"; +import MainForm from "./index"; +import addSnapshotSerializer from "../../utils/snapshot"; + +const renderSUMS = async () => { + const { container } = render( + + + + + + + + + + + + ); + await waitFor(() => { + /* render */ + }); + return container; +}; + +describe("SUMS", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + setToken(TEST_AUTH_OBJECT_FISKER); + const container = await renderSUMS(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/services/updatesAPI.js b/src/services/updatesAPI.js index 83f0736..adce177 100644 --- a/src/services/updatesAPI.js +++ b/src/services/updatesAPI.js @@ -122,7 +122,43 @@ const updatesAPI = { }) .then(fetchRespHandler) .catch(errorHandler); - } + }, + + getSUMSVersionRxSwins: async (sums_version, options, token) => { + return fetch(addQueryParams(`${API_ENDPOINT}/manifest/sums/${sums_version}/rxswins`, options), { + method: "GET", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + }) + .then(fetchRespHandler) + .catch(errorHandler); + }, + + deleteSUMSVersionRxSwins: async (sums_version, rxswin, token) => { + return fetch(`${API_ENDPOINT}/manifest/sums/${sums_version}/rxswins/${rxswin}`, { + method: "DELETE", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + }) + .then(fetchRespHandler) + .catch(errorHandler); + }, + + addSUMSVersionRxSwins: async (sums_version, data, token) => + fetch(`${API_ENDPOINT}/manifest/sums/${sums_version}/rxswins`, { + method: "POST", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + body: JSON.stringify(data), + }) + .then(fetchRespHandler) + .catch(errorHandler), }; export default updatesAPI; \ No newline at end of file diff --git a/src/utils/roles.js b/src/utils/roles.js index fb8b43c..3a3ecbd 100644 --- a/src/utils/roles.js +++ b/src/utils/roles.js @@ -100,6 +100,11 @@ export const Permissions = { [Providers.FISKER_QA]: [Roles.MANUFACTURE], [Providers.MAGNA]: [Roles.MAGNAGROUP], }, + FiskerMagnaDelete: { + [Providers.FISKER]: [Roles.DELETE], + [Providers.FISKER_QA]: [Roles.MANUFACTURE], + [Providers.MAGNA]: [Roles.MAGNAGROUP], + }, ManifestMigration: { [Providers.FISKER]: [Roles.MANIFEST_MIGRATION] }, diff --git a/src/utils/roles.test.js b/src/utils/roles.test.js index 9b40e97..0cc90fd 100644 --- a/src/utils/roles.test.js +++ b/src/utils/roles.test.js @@ -113,6 +113,18 @@ describe("Roles Helper", () => { ).toEqual(true); }); + it("Check FiskerMagnaDelete permission", () => { + expect( + hasRole([Roles.DELETE], Permissions.FiskerMagnaDelete, [Providers.FISKER]) + ).toEqual(true); + expect( + hasRole([Roles.MAGNAGROUP], Permissions.FiskerMagnaDelete, [Providers.MAGNA]) + ).toEqual(true); + expect( + hasRole([Roles.MANUFACTURE], Permissions.FiskerMagnaDelete, [Providers.FISKER_QA]) + ).toEqual(true); + }); + it("Get groups", () => { expect(getGroups(TEST_TOKEN)).toEqual(TEST_EXPECTED_GROUPS); });