From 7dff3be1da7d296ae192b447e98d67adb950bad1 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Tue, 18 Jul 2023 12:26:39 -0400 Subject: [PATCH 01/12] CEC-4674: add bulk cancel updates (#386) * add bulk cancel updates * add permission check * remove unused import * make trigger multi-line --- .../App/__snapshots__/App.test.js.snap | 354 ++++++++++++++---- src/components/Manifest/Status/index.jsx | 170 +++++---- .../Toolbar/__snapshots__/index.test.jsx.snap | 53 +++ .../Manifest/Toolbar/actions/Cancel.jsx | 55 +++ .../Manifest/Toolbar/actions/Cancel.test.jsx | 40 ++ src/components/Manifest/Toolbar/index.jsx | 71 ++++ .../Manifest/Toolbar/index.test.jsx | 77 ++++ 7 files changed, 683 insertions(+), 137 deletions(-) create mode 100644 src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Manifest/Toolbar/actions/Cancel.jsx create mode 100644 src/components/Manifest/Toolbar/actions/Cancel.test.jsx create mode 100644 src/components/Manifest/Toolbar/index.jsx create mode 100644 src/components/Manifest/Toolbar/index.test.jsx diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index c6a7b02..7689412 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -5301,14 +5301,63 @@ exports[`App Route /package-status authenticated 1`] = `
-
+
+
+ +
+
+
+ + +
+
+
@@ -5319,39 +5368,160 @@ exports[`App Route /package-status authenticated 1`] = ` class="MuiTableRow-root MuiTableRow-head" > + - + - + - + - { const { manifest_id } = useParams(); const classes = useStyles(); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); const [pageIndex, setPageIndex] = useState(0); + const [orderBy, setOrderBy] = useState("id"); + const [order, setOrder] = useState("asc"); + const [ids, setIds] = useState([]); const { getManifests, manifests } = useManifestsContext(); const { - cancelUpdate, getCarUpdates, carUpdates, totalCarUpdates, @@ -54,10 +78,35 @@ const MainForm = () => { token: { idToken: { jwtToken: token }, }, - groups, - providers, } = useUserContext(); + const handleSelectAll = () => { + setIds((ids) => ids.length === 0 + ? carUpdates.map((carUpdate) => carUpdate.id) + : []); + } + + const handleSelect = (newId, selected) => { + if (selected) { + setIds((ids) => ids.filter((id) => id !== newId)); + } else { + setIds((ids) => [...ids, newId]); + } + } + + const handleSort = (_event, property) => { + if (property === orderBy) { + if (order === "asc") { + setOrder("desc"); + } else { + setOrder("asc"); + } + } else { + setOrderBy(property); + setOrder("asc"); + } + }; + useEffect(() => { (async () => { try { @@ -100,6 +149,7 @@ const MainForm = () => { manifest_id, limit: pageSize, offset: pageSize * pageIndex, + order: `${orderBy} ${order}`, }, token ); @@ -109,7 +159,7 @@ const MainForm = () => { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageIndex, pageSize, token]); + }, [pageIndex, pageSize, token, order, orderBy]); useEffect(() => { try { @@ -134,63 +184,58 @@ const MainForm = () => { setPageIndex(0); }; - const sendCancel = async ({ id, vin }) => { - try { - await cancelUpdate(id, token); - setMessage(`Sent cancel for ${vin}`); - } catch (e) { - setMessage(e.message); - } - }; - return (
+ + + + + + + +
+ + + + + + + + - ID + + ID + + sorted ascending + + + - Vehicle + + Vehicle + + - Status + + Status + + - Created + + Created + + - Updated + + Updated + +
+ + + + + + + + @@ -5390,30 +5592,42 @@ exports[`App Route /package-status authenticated 1`] = ` > 7/12/2021 6:22:13 PM - - - -
+ + + + + + + + @@ -5444,30 +5658,42 @@ exports[`App Route /package-status authenticated 1`] = ` > 7/12/2021 6:22:13 PM - - - -
+ + + + + + + + @@ -5498,26 +5724,6 @@ exports[`App Route /package-status authenticated 1`] = ` > 7/12/2021 6:22:13 PM - - - -
- - - ID - Vehicle - Status - Created - Updated - - - + - {carUpdates.map((row) => ( - - {row.id} - - {row.vin} - - - {row.status} - {row.progress > -1 && ( - - )} - - - {LocalDateTimeString(row.created)} - - - {LocalDateTimeString(row.updated)} - - - No action} - > - - sendCancel(row)}> - - - - - - - ))} + {carUpdates.map((row) => { + const isSelected = ids.indexOf(row.id) !== -1; + return ( + + + handleSelect(row.id, isSelected)} + /> + + {row.id} + + {row.vin} + + + {row.status} + {row.progress > -1 && ( + + )} + + + {LocalDateTimeString(row.created)} + + + {LocalDateTimeString(row.updated)} + + + ) + })} @@ -217,7 +262,6 @@ const MainForm = () => { const ManifestStatus = () => ( - diff --git a/src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap b/src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..eec1c36 --- /dev/null +++ b/src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Toolbar Render 1`] = ` +
+
+
+
+ + +
+
+
+
+`; diff --git a/src/components/Manifest/Toolbar/actions/Cancel.jsx b/src/components/Manifest/Toolbar/actions/Cancel.jsx new file mode 100644 index 0000000..2a815dc --- /dev/null +++ b/src/components/Manifest/Toolbar/actions/Cancel.jsx @@ -0,0 +1,55 @@ +import { forwardRef, useImperativeHandle } from "react"; +import { useStatusContext } from "../../../Contexts/StatusContext"; +import { useUserContext } from "../../../Contexts/UserContext"; +import TaskRunner from "../../../../utils/taskRunner"; +import updatesAPI from "../../../../services/updatesAPI"; + +export default forwardRef(({ + ids, + idCSV, +}, ref) => { + const { setMessage } = useStatusContext(); + const { token: { idToken: { jwtToken: token } } } = useUserContext(); + + useImperativeHandle(ref, () => ({ + async submit() { + return new Promise((resolve, reject) => { + const taskRunner = new TaskRunner(5, ids.length); + let errorCount = 0; + + const task = (id, index) => { + const progressMessage = `${index + 1}/${ids.length}`; + return async () => updatesAPI.cancelCarUpdate(id, token) + .then((response) => { + if (response.error) { + errorCount += 1; + setMessage(`${progressMessage} ${response.error}: ${response.message}`); + } else { + setMessage(`${progressMessage} Canceled update ${id}`); + } + return response; + }) + .catch((error) => reject(error)); + } + + ids.forEach((id, i) => { + taskRunner.push(task(id, i)); + }); + + taskRunner.onComplete().then((responses) => { + const completeMessage = `${ids.length - errorCount}/${ids.length}`; + setMessage(`Successfully canceled ${completeMessage} updates.`); + resolve(responses); + }); + }); + }, + })); + + return ( +
+

+ You are canceling the following updates: {idCSV}. +

+
+ ); +}); \ No newline at end of file diff --git a/src/components/Manifest/Toolbar/actions/Cancel.test.jsx b/src/components/Manifest/Toolbar/actions/Cancel.test.jsx new file mode 100644 index 0000000..1fa05b3 --- /dev/null +++ b/src/components/Manifest/Toolbar/actions/Cancel.test.jsx @@ -0,0 +1,40 @@ +jest.mock("../../../Contexts/UserContext"); +jest.mock("../../../Contexts/StatusContext"); +jest.mock("../../../../services/updatesAPI"); + +import React from "react"; +import { + render, + act, +} from "@testing-library/react"; +import { UserProvider, setToken } from "../../../Contexts/UserContext"; +import { StatusProvider } from "../../../Contexts/StatusContext"; +import { TEST_AUTH_OBJECT_FISKER } from "../../../../utils/testing"; +import Cancel from "./Cancel"; +import updatesAPI from "../../../../services/updatesAPI"; + +describe("Manifest/Cancel", () => { + beforeAll(() => { + setToken(TEST_AUTH_OBJECT_FISKER); + }); + + it("makes request to cancel an update", async () => { + const api = jest.spyOn(updatesAPI, "cancelCarUpdate"); + const ref = React.createRef(); + + render( + + + + + + ); + + await act(async () => ref.current.submit()); + expect(api).toHaveBeenCalledTimes(3); + }); +}); \ No newline at end of file diff --git a/src/components/Manifest/Toolbar/index.jsx b/src/components/Manifest/Toolbar/index.jsx new file mode 100644 index 0000000..b98d892 --- /dev/null +++ b/src/components/Manifest/Toolbar/index.jsx @@ -0,0 +1,71 @@ +import { useEffect, useState, useRef, Suspense, lazy } from "react"; +import DropDownButton from "../../Controls/DropDownButton"; +import { Modal } from "../../BulkActions/Modal"; +import { useUserContext } from "../../Contexts/UserContext"; +import { Permissions, hasRole } from "../../../utils/roles"; + +// Code-splitting individual actions +// https://react.dev/reference/react/lazy +const Cancel = lazy(() => import("./actions/Cancel")); + +export default function Toolbar({ + ids = [], + actions = [], +}) { + const [title, setTitle] = useState("Action"); + const [open, setOpen] = useState(false); + const [active, setActive] = useState(null); + const activeRef = useRef(); + const { groups, providers } = useUserContext(); + + const hasAccess = hasRole(groups, Permissions.FiskerMagnaCreate, providers); + + const filteredActions = [ + { + id: "cancel", + name: "Cancel Updates", + disabled: !hasAccess || ids.length <= 0, + trigger: () => { + setOpen(true); + setActive("cancel"); + }, + }, + ].filter((action) => actions.includes(action.id)); + + const payload = { + ids, + idCSV: ids.join(", "), + ref: activeRef + }; + + const handleClose = () => { + setOpen(false).then(() => setActive(null)); + } + + const handleSubmit = () => { + activeRef.current.submit(); + handleClose(); + } + + useEffect(() => { + setTitle(filteredActions.find((action) => active === action.id)?.name || "Action"); + }, [active, filteredActions]); + + return ( + <> + + + Loading...}> +
+ {active === "cancel" && } +
+
+
+ + ) +} \ No newline at end of file diff --git a/src/components/Manifest/Toolbar/index.test.jsx b/src/components/Manifest/Toolbar/index.test.jsx new file mode 100644 index 0000000..1fe9bbd --- /dev/null +++ b/src/components/Manifest/Toolbar/index.test.jsx @@ -0,0 +1,77 @@ +jest.mock("../../Contexts/UserContext"); +jest.mock("../../Contexts/StatusContext"); + +import React from "react"; +import { + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; +import { UserProvider, setToken } from "../../Contexts/UserContext"; +import { StatusProvider } from "../../Contexts/StatusContext"; +import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; +import Toolbar from "."; +import addSnapshotSerializer from "../../../utils/snapshot"; + +describe("Toolbar", () => { + beforeAll(() => { + setToken(TEST_AUTH_OBJECT_FISKER); + global.URL.createObjectURL = jest.fn(); + global.URL.revokeObjectURL = jest.fn(); + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + const { container } = render( + + + + + + ); + await waitFor(() => { + /* render */ + }); + expect(container).toMatchSnapshot(); + }); + + it("opens a modal", async () => { + render( + + + + + + ); + + const buttonEl = screen.getByText("Cancel Updates"); + fireEvent.click(buttonEl); + const submitEl = screen.getByText("Submit"); + expect(submitEl).toBeTruthy(); + }); + + it("filters valid actions", async () => { + render( + + + + + + ); + + const dropdownBtn = screen.getByTestId("dropdown-button-expand"); + fireEvent.click(dropdownBtn); + const dropdownOptions = screen.getAllByRole("menuitem"); + expect(dropdownOptions.length).toBe(1); + }); +}); \ No newline at end of file From e767e6dfddbb14935872b66f2fb0388b054133c3 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Tue, 18 Jul 2023 12:50:27 -0400 Subject: [PATCH 02/12] CEC-4546: add ecu search (#391) * CEC-4546: add ecu search * CEC-4546: add checkbox to filter current * add column * set initial state to true for unique --- src/components/Cars/Status/ECUsTab.jsx | 38 ++++- .../__snapshots__/ECUsTab.test.jsx.snap | 134 ++++++++++++++++++ .../Controls/CarECUsTable/index.jsx | 10 +- src/components/useStyles.jsx | 8 +- 4 files changed, 184 insertions(+), 6 deletions(-) diff --git a/src/components/Cars/Status/ECUsTab.jsx b/src/components/Cars/Status/ECUsTab.jsx index b966407..276f35a 100644 --- a/src/components/Cars/Status/ECUsTab.jsx +++ b/src/components/Cars/Status/ECUsTab.jsx @@ -1,13 +1,22 @@ -import { Typography } from "@material-ui/core"; +import { + Typography, + Grid, + FormControlLabel, + Checkbox, +} from "@material-ui/core"; import clsx from "clsx"; -import React from "react"; +import React, { useState } from "react"; import { useUserContext } from "../../Contexts/UserContext"; import { VehicleProvider } from "../../Contexts/VehicleContext"; +import { useLocalStorage } from "../../useLocalStorage"; import CarECUsTable from "../../Controls/CarECUsTable"; +import SearchField from "../../Controls/SearchField"; import useStyles from "../../useStyles"; const MainForm = ({ vin }) => { + const [search, setSearch] = useLocalStorage("ECU_SEARCH", ""); + const [unique, setUnique] = useState(true); const classes = useStyles(); const { token: { @@ -15,12 +24,35 @@ const MainForm = ({ vin }) => { }, } = useUserContext(); + const handleSearch = (query) => { + setSearch(query); + }; + + const handleUnique = () => { + setUnique(unique => !unique); + } + return (
Car ECUs - + + + + } + className={classes.noWrap} + /> + + +
); }; diff --git a/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap index b37ef2e..93e63b1 100644 --- a/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/ECUsTab.test.jsx.snap @@ -16,6 +16,101 @@ exports[`ECUsTab Render 1`] = ` > Car ECUs +
+
+
+ +
+ +
+ +
+
+
+ +
+
@@ -264,6 +359,29 @@ exports[`ECUsTab Render 1`] = ` +
7/14/2021 8:09:40 PM + 7/14/2021 8:09:40 PM + { +const CarECUsTable = ({ vin, token, classes, search, unique }) => { const [ecus, setECUs] = useState([]); const [total, setTotal] = useState(0); const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10); @@ -79,6 +83,8 @@ const CarECUsTable = ({ vin, token, classes }) => { const result = await getECUs( { vin, + search, + unique, limit: pageSize, offset: pageSize * pageIndex, order: `${orderBy} ${order}`, @@ -93,7 +99,7 @@ const CarECUsTable = ({ vin, token, classes }) => { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [vin, token, pageIndex, pageSize, orderBy, order]); + }, [vin, token, pageIndex, pageSize, orderBy, order, search, unique]); const handleChangePageIndex = (event, newIndex) => { setPageIndex(newIndex); diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx index 40a9bba..7c598df 100644 --- a/src/components/useStyles.jsx +++ b/src/components/useStyles.jsx @@ -312,7 +312,13 @@ const useStyles = makeStyles((theme) => ({ display: "flex", alignItems: "center", gap: "12px", - } + }, + flex: { + display: "flex", + }, + noWrap: { + whiteSpace: "nowrap", + }, })); export default useStyles; From c572e5de5b8e0c25c472e28620cd146ff99f184e Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Tue, 18 Jul 2023 17:19:49 -0400 Subject: [PATCH 03/12] CEC-4745: add vins nullcheck for bulk actions bulk actions (#395) * nullcheck bulk actions * remove unused var * update snapshots --- .../App/__snapshots__/App.test.js.snap | 39 ------------------- src/components/BulkActions/index.jsx | 4 +- .../List/__snapshots__/index.test.jsx.snap | 39 ------------------- .../__snapshots__/ECUsTab.test.jsx.snap | 5 ++- 4 files changed, 6 insertions(+), 81 deletions(-) diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 7689412..7ba10d0 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -12501,45 +12501,6 @@ exports[`App Route /vehicles authenticated 1`] = ` /> -
- - -
0) ? vins.join(", ") : "N/A", ref: activeRef }; @@ -63,6 +63,8 @@ export default function BulkActions({ setTitle(filteredActions.find((action) => active === action.id)?.name || "Action"); }, [active, filteredActions]); + if (!vins || vins.length === 0) return <>; + return ( <> diff --git a/src/components/Cars/List/__snapshots__/index.test.jsx.snap b/src/components/Cars/List/__snapshots__/index.test.jsx.snap index c7dacb1..1c5b5f3 100644 --- a/src/components/Cars/List/__snapshots__/index.test.jsx.snap +++ b/src/components/Cars/List/__snapshots__/index.test.jsx.snap @@ -37,45 +37,6 @@ exports[`VehicleTable Render 1`] = ` /> -
- - -
From d6d1b3107ed987c3f317290eb5bc99cc947bcee3 Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:01:39 -0400 Subject: [PATCH 04/12] CEC-4772 - carsconnected takes VINs in req body (#397) --- src/components/Contexts/FleetContext.jsx | 2 +- src/components/Contexts/VehicleContext.jsx | 4 ++-- src/services/__mocks__/vehiclesAPI.js | 2 +- src/services/vehiclesAPI.js | 5 +++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Contexts/FleetContext.jsx b/src/components/Contexts/FleetContext.jsx index 551a762..70d58b4 100644 --- a/src/components/Contexts/FleetContext.jsx +++ b/src/components/Contexts/FleetContext.jsx @@ -113,7 +113,7 @@ export const FleetProvider = ({ children }) => { throw new Error(`Get fleet vehicles error. ${result.message}`); } - const connectionsResult = await vehiclesAPI.getConnections(result.data, token) + const connectionsResult = await vehiclesAPI.getConnections({ "VINs": result.data }, token) if (result.error) { setFleetVehicles([]) throw new Error(`Get vehicles connections error. ${result.message}`); diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 261a2ab..ba4458d 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -35,7 +35,7 @@ export const VehicleProvider = ({ children }) => { try { if (cars.length === 0) return; const vins = cars.map((car) => car.vin); - const result = await api.getConnections(vins, token); + const result = await api.getConnections({ "VINs": vins }, token); if (result.error) { throw new Error(`Add connections error. ${result.message}`); @@ -80,7 +80,7 @@ export const VehicleProvider = ({ children }) => { const getConnections = async (vins, token) => { try { setBusy(true); - const result = await api.getConnections(vins, token); + const result = await api.getConnections({ "VINs": vins }, token); if (result.error) throw new Error(`Get connections error. ${result.message}`); return result; diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index 3dcad50..ca737c9 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -116,7 +116,7 @@ const vehiclesAPI = { getConnections: async (vins) => { const result = {}; - vins.forEach((vin) => { + vins.VINs.forEach((vin) => { result[vin] = true; result["2:" + vin] = false; }); diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index fc398ce..6fe7356 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -41,13 +41,14 @@ const vehiclesAPI = { .catch(errorHandler), getConnections: async (vins, token) => { - const u = `${API_ENDPOINT}/carsconnected?vins=${vins.join(",")}`; + const u = `${API_ENDPOINT}/carsconnected`; return fetch(u, { - method: "GET", + method: "POST", headers: Object.assign( { "Content-Type": "application/json" }, getAuthHeaderOptions(token) ), + body: JSON.stringify(vins), }) .then(fetchRespHandler) .catch(errorHandler); From 242df54ee49bca8c518d5a8af785db56ddc9d172 Mon Sep 17 00:00:00 2001 From: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:40:35 -0700 Subject: [PATCH 05/12] CEC-4781 remove duplicates from Remote Reset dropdown. (#398) * CEC-4781 remove duplicates from Remote Reset dropdown. * fix --- src/components/Controls/SendDiagnosticCommand/index.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Controls/SendDiagnosticCommand/index.jsx b/src/components/Controls/SendDiagnosticCommand/index.jsx index 9e946c4..4784971 100644 --- a/src/components/Controls/SendDiagnosticCommand/index.jsx +++ b/src/components/Controls/SendDiagnosticCommand/index.jsx @@ -46,7 +46,13 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => { sortECUs(result.data) result.data.push({ ecu: "TBOX" }) setCurrentECU(result.data[0].ecu) - setEcus(result.data) + function removeDuplicatesByField(arr, field) { + const uniqueArray = arr.filter((item, index, self) => { + return index === self.findIndex((obj) => obj[field] === item[field]); + }); + return uniqueArray; + } + setEcus(removeDuplicatesByField(result.data, "ecu")) })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [vin]); From d562250a1373b1e6adc95bf6920ea00d79072af1 Mon Sep 17 00:00:00 2001 From: Rafi Greenberg <72412693+rafi-fisker@users.noreply.github.com> Date: Tue, 25 Jul 2023 15:49:12 -0800 Subject: [PATCH 06/12] might work (#400) --- .github/workflows/deploy-on-demand.yml | 114 +++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .github/workflows/deploy-on-demand.yml diff --git a/.github/workflows/deploy-on-demand.yml b/.github/workflows/deploy-on-demand.yml new file mode 100644 index 0000000..52c23e2 --- /dev/null +++ b/.github/workflows/deploy-on-demand.yml @@ -0,0 +1,114 @@ +name: OTA Portal Deploy - On Demand + +on: + workflow_dispatch: + inputs: + environment: + description: "Environment" + required: true + type: choice + options: + - dev + - stage + - preprod + +env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_CHANNEL: "#cloud-builds" + SLACK_FOOTER: "" + SLACK_USERNAME: GitHub Actions + SLACK_ICON: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" + TAG: ${{ github.sha }} + PROJECT: ota-admin-portal + REGISTRY: fiskercloud.azurecr.io + +jobs: + build: + runs-on: ubuntu-latest + outputs: + build-env: ${{ steps.set-env.outputs.ENVIRONMENT }} + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + + - name: Checkout + uses: actions/checkout@v3 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.AZURE_CLIENT_ID }} + password: ${{ secrets.AZURE_CLIENT_SECRET }} + + - name: Set Env + env: + ENV: ${{ inputs.environment }} + id: set-env + run: | + case ${ENV} in + dev) + ENVIRONMENT=dev;; + stage) + ENVIRONMENT=stg;; + preprod) + ENVIRONMENT=prd;; + *) + ENVIRONMENT=dev;; + esac + echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_ENV + echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + build-args: ENVIRONMENT=${{ env.ENVIRONMENT }} + push: true + tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-${{ env.ENVIRONMENT }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: [self-hosted, azure] + env: + ENVIRONMENT: ${{ needs.build.outputs.build-env }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to ${{ inputs.environment }}... :partydeploy:" + + - name: Deploy + run: |- + helm upgrade \ + --kube-context $ENVIRONMENT \ + --set image.registry=$REGISTRY \ + --set image.name=$PROJECT \ + --set image.tag=$TAG-$ENVIRONMENT \ + --wait -i -f k8s/values-$ENVIRONMENT.yaml $PROJECT k8s/ + + - name: Notify deploy + uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to ${{ inputs.environment }}! :gopher_party:" + + - name: Notify if failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} to ${{ inputs.environment }}! :this-is-fine:" From 28fe37712ffa2dc9660115209a4d729392a4845d Mon Sep 17 00:00:00 2001 From: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:06:52 -0700 Subject: [PATCH 07/12] CEC-4781 fix duplicate ECUs (correct way) (#403) --- .../Controls/SendDiagnosticCommand/index.jsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/Controls/SendDiagnosticCommand/index.jsx b/src/components/Controls/SendDiagnosticCommand/index.jsx index 4784971..c054953 100644 --- a/src/components/Controls/SendDiagnosticCommand/index.jsx +++ b/src/components/Controls/SendDiagnosticCommand/index.jsx @@ -42,17 +42,12 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => { useEffect(() => { (async () => { if (!vin) return; - const result = await getECUs({ vin }, token) + const unique = true; + const result = await getECUs({ vin, unique }, token) sortECUs(result.data) result.data.push({ ecu: "TBOX" }) setCurrentECU(result.data[0].ecu) - function removeDuplicatesByField(arr, field) { - const uniqueArray = arr.filter((item, index, self) => { - return index === self.findIndex((obj) => obj[field] === item[field]); - }); - return uniqueArray; - } - setEcus(removeDuplicatesByField(result.data, "ecu")) + setEcus(result.data) })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [vin]); From 56dd4a0c8f3fb7a31625e31abf1742b3db4f6b77 Mon Sep 17 00:00:00 2001 From: Eduard Voronkin <116690094+eduardvoronkin@users.noreply.github.com> Date: Fri, 28 Jul 2023 10:45:31 -0700 Subject: [PATCH 08/12] CEC-4809 allow Remote Diagnostic for "dev" T.Rexes. (#405) --- src/components/Controls/SendDiagnosticCommand/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Controls/SendDiagnosticCommand/index.jsx b/src/components/Controls/SendDiagnosticCommand/index.jsx index c054953..48f8466 100644 --- a/src/components/Controls/SendDiagnosticCommand/index.jsx +++ b/src/components/Controls/SendDiagnosticCommand/index.jsx @@ -71,7 +71,7 @@ const SendDiagnosticCommand = ({ vin, token, classes }) => { const TREX_MIN_VER = "1.1.141"; const isRemoteResetSupported = () => { - return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) >= 0; + return !carState?.trex_version ? true : cmp(carState.trex_version, TREX_MIN_VER) >= 0 || carState.trex_version.includes("dev"); }; const clickHandler = async (_) => { From 5716832a81c4aa0d80333f33b31be8492ad7dbd3 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Mon, 31 Jul 2023 11:08:23 -0400 Subject: [PATCH 09/12] CEC-4564: add trie select component (#404) * add TrieSelect * setup menu button * CEC-4564: add trie select component * CEC-4564: fix selectall bool check * update tests --- .../__snapshots__/index.test.jsx.snap | 97 +- .../CANSelfServe/SelfServe/index.jsx | 63 +- .../Controls/TrieSelect/TrieSelect.jsx | 205 ++++ .../Controls/TrieSelect/TrieSelect.test.tsx | 54 + .../Controls/TrieSelect/TrieSelectContext.js | 29 + .../__snapshots__/TrieSelect.test.tsx.snap | 1003 +++++++++++++++++ src/components/Controls/TrieSelect/index.js | 1 + src/components/Controls/TrieSelect/trie.js | 62 + .../Controls/TrieSelect/trie.test.js | 24 + src/components/useStyles.jsx | 7 + 10 files changed, 1460 insertions(+), 85 deletions(-) create mode 100644 src/components/Controls/TrieSelect/TrieSelect.jsx create mode 100644 src/components/Controls/TrieSelect/TrieSelect.test.tsx create mode 100644 src/components/Controls/TrieSelect/TrieSelectContext.js create mode 100644 src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap create mode 100644 src/components/Controls/TrieSelect/index.js create mode 100644 src/components/Controls/TrieSelect/trie.js create mode 100644 src/components/Controls/TrieSelect/trie.test.js diff --git a/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap b/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap index 7c0453b..5e55a76 100644 --- a/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap +++ b/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap @@ -339,57 +339,78 @@ exports[`Render Render 1`] = ` class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12" >
-
+
- - ​ - -
- - +
+
    { const classes = useStyles(); @@ -72,27 +73,19 @@ const MainForm = ({ id }) => { setSelectedEndDate(value); }; - const handleSelectedItemsChange = (event) => { - const { value } = event.target; - if (value.some(item => item === "Select All")) { - setSelectAllCanSignals(true); - if (selectedCanSignals.length === canSignals.length) { - setSelectedCanSignals([]); - } else { - setSelectedCanSignals(canSignals.map(signal => signal.signal_name)); - } - } else { - setSelectAllCanSignals(false); - setSelectedCanSignals(value); - } - }; - const displayTimeAsGMT = (date) => { return gmtTimezone - ? date.toLocaleString("en-US", {timeZone: "Etc/GMT"}) + ? date.toLocaleString("en-US", { timeZone: "Etc/GMT" }) : date; } + useEffect(() => { + if (canSignals.length === selectedCanSignals.length) { + setSelectAllCanSignals(true); + } else { + setSelectAllCanSignals(false); + } + }, [canSignals, selectedCanSignals, setSelectAllCanSignals]); return (
    @@ -175,36 +168,12 @@ const MainForm = ({ id }) => { - - Select CAN signals - - + signal.signal_name))} + onChange={setSelectedCanSignals} + /> + + } + /> + + + + + + +
      + {selected.map((signal) => { + return ( +
    • + remove(signal)} /> +
    • + ); + })} +
    + + ) +} + +const TrieSelectLevel = ({ + prefix = "", + node, + children, + classification +}) => { + const classes = useStyles(); + const { selected, add, remove } = useTrieSelect(); + const [open, setOpen] = React.useState(false); + const completeChildren = Object.values(node.children).filter(child => child.isComplete); + const descendantCount = `${node.count} ${classification}`; + + const handleExpand = () => { + setOpen(open => !open); + }; + + const handleCheck = (names = [], checked) => { + for (let i = 0; i < names.length; i++) { + if (checked) { + remove(names[i]); + } else { + add(names[i]) + } + } + } + + const getWords = (node, prepend = prefix, result = new Set()) => { + if (prepend === prefix && node.isComplete) result.add(prepend); + + for (const child of Object.values(node.children)) { + const word = (prepend ? prepend + "_" : "") + child.data; + if (child.isComplete) { + result.add(word); + } + if (child.children) { + getWords(child, word, result); + } + } + + return Array.from(result); + } + + const listItems = ( + <> + {children} + {Object.values(node.children).map((child) => { + const fullName = (prefix ? prefix + "_" : "") + child.data; + const isChecked = selected.includes(fullName); + return ( + + {child.isComplete && ( + + + handleCheck([fullName], isChecked)} /> + + + + )} + + ); + })} + + ); + + const isParentOfMultiple = completeChildren.length > 1; + const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1; + if (isParentOfMultiple || isAdoptiveParentOfMultiple) { + const allDescendants = getWords(node, prefix); + const isSelectAll = allDescendants.every(descendant => selected.includes(descendant)); + return ( + <> + + + + + handleCheck(allDescendants, isSelectAll)} + /> + } + /> + {open ? : } + + + + + + {listItems} + + + + ) + } + + return listItems; +} diff --git a/src/components/Controls/TrieSelect/TrieSelect.test.tsx b/src/components/Controls/TrieSelect/TrieSelect.test.tsx new file mode 100644 index 0000000..e2d2bc8 --- /dev/null +++ b/src/components/Controls/TrieSelect/TrieSelect.test.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import userEvent from '@testing-library/user-event'; + +import { TrieSelect } from "."; +import addSnapshotSerializer from "../../../utils/snapshot"; + +const options = [ + "ace", + "ace_bev_one", + "ace_bev_two", + "ace_bev_three", + "bev", + "bev_chaz_deep", + "bev_chaz_deep_one", + "bev_chaz_deep_two", +]; + +describe("TrieSelect", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + const { container } = render( + + ); + await waitFor(() => { + /* render */ + }); + expect(container).toMatchSnapshot(); + }); + + it("properly passes payload to callback", async () => { + const mockCallback = jest.fn(); + + const { getByText } = render( + + ); + + const selectAll = getByText("Select All 8"); + userEvent.click(selectAll); + expect(mockCallback).toHaveBeenCalledWith(options); + }); +}); diff --git a/src/components/Controls/TrieSelect/TrieSelectContext.js b/src/components/Controls/TrieSelect/TrieSelectContext.js new file mode 100644 index 0000000..91885c8 --- /dev/null +++ b/src/components/Controls/TrieSelect/TrieSelectContext.js @@ -0,0 +1,29 @@ +import { createContext, useState, useContext, useEffect } from "react"; + +const TrieSelectContext = createContext(); + +export const useTrieSelect = () => { + const context = useContext(TrieSelectContext); + if (context === undefined) { + throw new Error("useTrieSelect must be used within a TrieSelectProvider"); + } + + return context; +} + +export const TrieSelectProvider = ({ children, onChange }) => { + const [selected, setSelected] = useState([]); + + const add = (id) => setSelected(prev => [...prev, id]); + const remove = (id) => setSelected(prev => prev.filter(select => select !== id)); + + useEffect(() => { + onChange(selected); + }, [onChange, selected]); + + return ( + + {children} + + ); +} diff --git a/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap b/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap new file mode 100644 index 0000000..ae32ef1 --- /dev/null +++ b/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap @@ -0,0 +1,1003 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TrieSelect Render 1`] = ` +
    +
    + + +
    +
    +
    +
    +
      +
    • +
      +
      + +

      + 8 Signal +

      +
      +
      +
      +
      + + +
      +
      +
    • +
      +
      +
      +
        +
      • +
        +
        + + ace + +

        + 4 Signal +

        +
        +
        +
        +
        + + +
        +
        +
      • +
        +
        +
        +
          +
        • +
          + + + + + + + +
          +
          + + ace + +
          +
        • +
        • +
          +
          + + ace_bev + +

          + 3 Signal +

          +
          +
          +
          +
          + + +
          +
          +
        • +
          +
          +
          +
            +
          • +
            + + + + + + + +
            +
            + + ace_bev_one + +
            +
          • +
          • +
            + + + + + + + +
            +
            + + ace_bev_two + +
            +
          • +
          • +
            + + + + + + + +
            +
            + + ace_bev_three + +
            +
          • +
          +
          +
          +
          +
        +
        +
        +
        +
      • +
        +
        + + bev + +

        + 4 Signal +

        +
        +
        +
        +
        + + +
        +
        +
      • +
        +
        +
        +
          +
        • +
          + + + + + + + +
          +
          + + bev + +
          +
        • +
        • +
          +
          + + bev_chaz + +

          + 3 Signal +

          +
          +
          +
          +
          + + +
          +
          +
        • +
          +
          +
          +
            +
          • +
            +
            + + bev_chaz_deep + +

            + 3 Signal +

            +
            +
            +
            +
            + + +
            +
            +
          • +
            +
            +
            +
              +
            • +
              + + + + + + + +
              +
              + + bev_chaz_deep + +
              +
            • +
            • +
              + + + + + + + +
              +
              + + bev_chaz_deep_one + +
              +
            • +
            • +
              + + + + + + + +
              +
              + + bev_chaz_deep_two + +
              +
            • +
            +
            +
            +
            +
          +
          +
          +
          +
        +
        +
        +
        +
      +
      +
      +
      +
    +
    +
    +
    +
      +
    +`; diff --git a/src/components/Controls/TrieSelect/index.js b/src/components/Controls/TrieSelect/index.js new file mode 100644 index 0000000..4b88811 --- /dev/null +++ b/src/components/Controls/TrieSelect/index.js @@ -0,0 +1 @@ +export { TrieSelect } from "./TrieSelect"; \ No newline at end of file diff --git a/src/components/Controls/TrieSelect/trie.js b/src/components/Controls/TrieSelect/trie.js new file mode 100644 index 0000000..4a6e344 --- /dev/null +++ b/src/components/Controls/TrieSelect/trie.js @@ -0,0 +1,62 @@ +class Node { + #count + + constructor(data, isComplete = false) { + this.data = data; + this.children = {}; + this.isComplete = isComplete; + this.#count = 0; + } + + get count() { + return this.#count; + } + + incrementCount() { + this.#count += 1; + } +} + +class Trie { + constructor(words = []) { + this.root = new Node(); + this.populate(words); + } + + populate(words) { + for (const word of words) { + this.add(word); + } + } + + add(parts, node = this.root) { + if (typeof parts === "string") { + parts = parts.split("_"); + } + + node.incrementCount(); + + if (parts.length === 0) { + node.isComplete = true; + return; + } + + const part = parts.shift(); + + if (node.children[part]) { + this.add(parts, node.children[part]); + } else { + const newNode = new Node(part); + node.children[part] = newNode; + this.add(parts, newNode); + } + } + + getRoot() { + return this.root; + } +} + +export { + Trie, +}; diff --git a/src/components/Controls/TrieSelect/trie.test.js b/src/components/Controls/TrieSelect/trie.test.js new file mode 100644 index 0000000..800cc0d --- /dev/null +++ b/src/components/Controls/TrieSelect/trie.test.js @@ -0,0 +1,24 @@ +import { Trie } from "./trie"; + +describe("Trie", () => { + it("adds words from instantiation", () => { + const trie = new Trie(["AAA_BBB_CCC"]); + const { children: { AAA: root } } = trie.getRoot(); + + expect(root.data).toBe("AAA"); + expect(root.children["BBB"].data).toBe("BBB"); + expect(root.children["BBB"].isComplete).toBe(false); + expect(root.children["BBB"].children["CCC"].data).toBe("CCC"); + expect(root.children["BBB"].children["CCC"].isComplete).toBe(true); + }); + + it("adds words with add method", () => { + const trie = new Trie(); + trie.add("AAA_BBB_CCC"); + const { children: { AAA: root } } = trie.getRoot(); + + expect(root.data).toBe("AAA"); + expect(root.children["BBB"].data).toBe("BBB"); + expect(root.children["BBB"].children["CCC"].data).toBe("CCC"); + }); +}); diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx index 7c598df..4f246d4 100644 --- a/src/components/useStyles.jsx +++ b/src/components/useStyles.jsx @@ -313,6 +313,13 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: "12px", }, + chipList: { + display: "flex", + gap: "4px 8px", + flexWrap: "wrap", + listStyleType: "none", + paddingLeft: 0, + }, flex: { display: "flex", }, From c118f676eef993e3935ad902ae1c5d156af6a9f9 Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Tue, 1 Aug 2023 13:53:36 -0400 Subject: [PATCH 10/12] CEC-4814 - Refactor vehicle_path endpoint (#407) --- src/components/Contexts/VehicleContext.jsx | 4 ++-- src/components/VehiclePathsMap/index.jsx | 11 ++--------- src/services/vehiclesAPI.js | 7 ++++--- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index ba4458d..909d7c3 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -112,10 +112,10 @@ export const VehicleProvider = ({ children }) => { } }; - const getLocationsVehiclePaths = async (token, vinsParam) => { + const getLocationsVehiclePaths = async (token, param, vins) => { try { setBusy(true); - const result = await api.getLocationsVehiclePaths(token, vinsParam); + const result = await api.getLocationsVehiclePaths(token, param, vins); if (result.error) throw new Error(`Get locations vehicle paths error. ${result.message}`); return result; diff --git a/src/components/VehiclePathsMap/index.jsx b/src/components/VehiclePathsMap/index.jsx index 10346e6..18929fd 100644 --- a/src/components/VehiclePathsMap/index.jsx +++ b/src/components/VehiclePathsMap/index.jsx @@ -44,16 +44,9 @@ const ComponentVehiclePathsMap = (props) => { const retrieveAndStoreLocations = (accessToken) => { let vinsToShowOnMap = [...props.vinsToShowOnMapColors.keys()]; - let vinsParam = "" - for (let vinToShowOnMap of vinsToShowOnMap) { - vinsParam += "vins=" - vinsParam += vinToShowOnMap - vinsParam += "&" - } - vinsParam += "lookback_hours=" - vinsParam += props.lookbackHours + let param = "lookback_hours=" + props.lookbackHours - return getLocationsVehiclePaths(accessToken, vinsParam) + return getLocationsVehiclePaths(accessToken, param, vinsToShowOnMap) .then(async (result) => { let resultArray = Object.entries(result) const points = [] diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index 6fe7356..40b3f5a 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -89,13 +89,14 @@ const vehiclesAPI = { .then(fetchRespHandler) .catch(errorHandler), - getLocationsVehiclePaths: async (token, vinsParam) => - fetch(`${API_ENDPOINT}/vehicle_paths?${vinsParam}`, { - method: "GET", + getLocationsVehiclePaths: async (token, param, vins) => + fetch(`${API_ENDPOINT}/vehicle_paths?${param}`, { + method: "POST", headers: Object.assign( { "Content-Type": "application/json" }, getAuthHeaderOptions(token) ), + body: JSON.stringify({ vins: vins }), }) .then(fetchRespHandler) .catch(errorHandler), From 27ffdf01b09eb69d0ce61212017f3f292176da0e Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Wed, 2 Aug 2023 15:44:08 -0400 Subject: [PATCH 11/12] CEC-4564: add visual nesting to CAN signal control (#406) * add TrieSelect * setup menu button * CEC-4564: add trie select component * add visual nesting * remove unused imports --- .../__snapshots__/index.test.jsx.snap | 73 +- .../CANSelfServe/SelfServe/index.jsx | 2 +- .../Controls/TrieSelect/TrieSelect.jsx | 86 +- .../Controls/TrieSelect/TrieSelect.test.tsx | 4 +- .../__snapshots__/TrieSelect.test.tsx.snap | 1620 +++++++++-------- src/components/Controls/TrieSelect/index.js | 2 +- src/components/useStyles.jsx | 1 + 7 files changed, 852 insertions(+), 936 deletions(-) diff --git a/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap b/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap index 5e55a76..907fc20 100644 --- a/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap +++ b/src/components/CANSelfServe/SelfServe/__snapshots__/index.test.jsx.snap @@ -338,76 +338,9 @@ exports[`Render Render 1`] = `
    -
    - - -
    -
    -
    -
    -
      -
    -
    -
    +
        diff --git a/src/components/CANSelfServe/SelfServe/index.jsx b/src/components/CANSelfServe/SelfServe/index.jsx index a7ae80b..723ebce 100644 --- a/src/components/CANSelfServe/SelfServe/index.jsx +++ b/src/components/CANSelfServe/SelfServe/index.jsx @@ -169,7 +169,7 @@ const MainForm = ({ id }) => { signal.signal_name))} onChange={setSelectedCanSignals} diff --git a/src/components/Controls/TrieSelect/TrieSelect.jsx b/src/components/Controls/TrieSelect/TrieSelect.jsx index a4a85f0..a62df9e 100644 --- a/src/components/Controls/TrieSelect/TrieSelect.jsx +++ b/src/components/Controls/TrieSelect/TrieSelect.jsx @@ -1,10 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; import { Box, - Button, Checkbox, Chip, Collapse, + Divider, FormControlLabel, List, ListItem, @@ -41,50 +41,19 @@ const TrieSelectList = ({ classification, options, }) => { - const { selected, setSelected, remove } = useTrieSelect(); - const [open, setOpen] = useState(false); + const { selected, remove } = useTrieSelect(); const classes = useStyles(); const trie = new Trie(options); - const handleExpand = () => { - setOpen(open => !open); - }; - - const handleSelectAll = () => { - setSelected((selected) => { - if (selected.length === 0) { - return options; - } - - return []; - }) - } - return ( <> - - - - } + + - - - - - - +
          {selected.map((signal) => { return ( @@ -102,13 +71,18 @@ const TrieSelectLevel = ({ prefix = "", node, children, - classification + classification, + label, + level = -1, }) => { const classes = useStyles(); const { selected, add, remove } = useTrieSelect(); const [open, setOpen] = React.useState(false); const completeChildren = Object.values(node.children).filter(child => child.isComplete); + const hasCompleteChildren = completeChildren.length > 0; const descendantCount = `${node.count} ${classification}`; + const isParentOfMultiple = completeChildren.length > 1; + const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1; const handleExpand = () => { setOpen(open => !open); @@ -140,7 +114,9 @@ const TrieSelectLevel = ({ return Array.from(result); } - const listItems = ( + const indent = (level) => `${12 * level}px`; + + const listItems = (level) => ( <> {children} {Object.values(node.children).map((child) => { @@ -152,13 +128,16 @@ const TrieSelectLevel = ({ node={child} key={fullName} classification={classification} + level={level} > {child.isComplete && ( - - handleCheck([fullName], isChecked)} /> - - + + + handleCheck([fullName], isChecked)} /> + + + )} @@ -167,15 +146,15 @@ const TrieSelectLevel = ({ ); - const isParentOfMultiple = completeChildren.length > 1; - const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1; if (isParentOfMultiple || isAdoptiveParentOfMultiple) { const allDescendants = getWords(node, prefix); const isSelectAll = allDescendants.every(descendant => selected.includes(descendant)); return ( <> - - + + + + } /> - {open ? : } + {open ? () : ()} - {listItems} + {listItems(level + 1)} + {hasCompleteChildren && } ) } - return listItems; + return listItems(level); } diff --git a/src/components/Controls/TrieSelect/TrieSelect.test.tsx b/src/components/Controls/TrieSelect/TrieSelect.test.tsx index e2d2bc8..b88fabf 100644 --- a/src/components/Controls/TrieSelect/TrieSelect.test.tsx +++ b/src/components/Controls/TrieSelect/TrieSelect.test.tsx @@ -38,7 +38,7 @@ describe("TrieSelect", () => { it("properly passes payload to callback", async () => { const mockCallback = jest.fn(); - const { getByText } = render( + const { getAllByText } = render( { /> ); - const selectAll = getByText("Select All 8"); + const selectAll = getAllByText("Select All")[0]; userEvent.click(selectAll); expect(mockCallback).toHaveBeenCalledWith(options); }); diff --git a/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap b/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap index ae32ef1..c58e306 100644 --- a/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap +++ b/src/components/Controls/TrieSelect/__snapshots__/TrieSelect.test.tsx.snap @@ -2,136 +2,56 @@ exports[`TrieSelect Render 1`] = `
          -
          - - -
          -
          -
          -
            -
          • -
            -
            +

            + 8 Signal +

            +
            +
            +
          +
          +
          +
          -
          -
          -
          - -
          -
          - -
          + + + + Select All + + +
          + +
          +
          + +
          +
          +
          +
            +
          • -
              -
            • -
              -
              - - ace - -

              - 4 Signal -

              -
              -
              -
              -
              - - -
              -
              -
            • -
              -
              -
                -
              • -
                - - - - - - - -
                -
                - - ace - -
                -
              • -
              • -
                -
                - - ace_bev - -

                - 3 Signal -

                -
                -
                -
                -
                - - -
                -
                -
              • -
                -
                -
                -
                  -
                • -
                  - - - - - - - -
                  -
                  - - ace_bev_one - -
                  -
                • -
                • -
                  - - - - - - - -
                  -
                  - - ace_bev_two - -
                  -
                • -
                • -
                  - - - - - - - -
                  -
                  - - ace_bev_three - -
                  -
                • -
                -
                -
                -
                -
              -
              -
              + ace + +

              + 4 Signal +

              -
            • +
            +
            +
            +
            +
          • +
            +
            +
            +
              +
            • - -
              - - + + ace + +
              -
            - -
            -
            +
          • -
              -
            • -
              + ace_bev + +

              + 3 Signal +

              +
              +
            +
          • +
            +
            +
            -
            - bev - -
            - -
          • -
            + + -
            - - bev_chaz - -

            - 3 Signal -

            -
            -
            -
            + + +
            +
          • + +
            +
            +
            +
              +
            • -
              +
              - Select All + ace_bev_one - +
              +
            + +
          • +
            +
            + + + + + + + +
            +
            + + ace_bev_two + +
            +
            +
          • +
          • +
            +
            + + + + + + + +
            +
            + + ace_bev_three + +
            +
            +
          • +
          +
          +
          +
          +
          +
        +
    +
    +
    +
  • +
    +
    +
    + + bev + +

    + 4 Signal +

    +
    +
    +
    +
    +
    + + +
    +
    +
  • +
    +
    +
    +
      +
    • +
      +
      + + + + + + + +
      +
      + + bev + +
      +
      +
    • +
    • +
      +
      +
      + + bev_chaz + +

      + 3 Signal +

      +
      +
      +
      +
      +
      +
      -
      -
    • -
      + + + + Select All + + +
      + +
      +
      + +
      +
      +
      +
        +
      • -
          -
        • -
          -
          +

          + 3 Signal +

          +
          +
          +
        +
        +
        +
        -
        -
        + + + + + + Select All + + + +
        +
      + +
      +
      +
      +
        +
      • - -
        - - + + bev_chaz_deep + +
        -
      - -
      -
      +
    • -
        -
      • -
        - + - - - - - -
        -
        - - bev_chaz_deep - -
        -
      • -
      • + + + +
      +
      + -
      - - - - - - - -
      -
      - - bev_chaz_deep_one - -
      -
    • -
    • -
      - - - - - - - -
      -
      - - bev_chaz_deep_two - -
      -
    • -
    + bev_chaz_deep_one + +
    -
    - -
+ +
  • +
    +
    + + + + + + + +
    +
    + + bev_chaz_deep_two + +
    +
    +
  • + +
    + - - + +
    + - - + + - - + +
    + - +
      diff --git a/src/components/Controls/TrieSelect/index.js b/src/components/Controls/TrieSelect/index.js index 4b88811..00bc9ca 100644 --- a/src/components/Controls/TrieSelect/index.js +++ b/src/components/Controls/TrieSelect/index.js @@ -1 +1 @@ -export { TrieSelect } from "./TrieSelect"; \ No newline at end of file +export * from "./TrieSelect"; diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx index 4f246d4..2cb95de 100644 --- a/src/components/useStyles.jsx +++ b/src/components/useStyles.jsx @@ -274,6 +274,7 @@ const useStyles = makeStyles((theme) => ({ maxWidth: "100%", }, whiteBackground: { backgroundColor: "White" }, + defaultBackground: { backgroundColor: "#fafafa" }, progressIcon: { width: 40, height: 40 }, progressSuccess: { color: "green" }, progressError: { color: "red" }, From fb6e91dcfce894e37c5687a330d6eeac028f2033 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Thu, 3 Aug 2023 10:04:34 -0400 Subject: [PATCH 12/12] CEC-4564: use Set for unique values (#408) * add TrieSelect * setup menu button * CEC-4564: add trie select component * CEC-4564: fix possible duplicate with set --- .../Controls/TrieSelect/TrieSelect.jsx | 12 +++++------- .../Controls/TrieSelect/TrieSelectContext.js | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/components/Controls/TrieSelect/TrieSelect.jsx b/src/components/Controls/TrieSelect/TrieSelect.jsx index a62df9e..b607fc3 100644 --- a/src/components/Controls/TrieSelect/TrieSelect.jsx +++ b/src/components/Controls/TrieSelect/TrieSelect.jsx @@ -58,7 +58,7 @@ const TrieSelectList = ({ {selected.map((signal) => { return (
    • - remove(signal)} /> + remove([signal])} />
    • ); })} @@ -89,12 +89,10 @@ const TrieSelectLevel = ({ }; const handleCheck = (names = [], checked) => { - for (let i = 0; i < names.length; i++) { - if (checked) { - remove(names[i]); - } else { - add(names[i]) - } + if (checked) { + remove(names); + } else { + add(names) } } diff --git a/src/components/Controls/TrieSelect/TrieSelectContext.js b/src/components/Controls/TrieSelect/TrieSelectContext.js index 91885c8..8011c8f 100644 --- a/src/components/Controls/TrieSelect/TrieSelectContext.js +++ b/src/components/Controls/TrieSelect/TrieSelectContext.js @@ -12,17 +12,26 @@ export const useTrieSelect = () => { } export const TrieSelectProvider = ({ children, onChange }) => { - const [selected, setSelected] = useState([]); + const [selected, setSelected] = useState(new Set()); - const add = (id) => setSelected(prev => [...prev, id]); - const remove = (id) => setSelected(prev => prev.filter(select => select !== id)); + const add = (ids) => setSelected(prev => new Set([...prev, ...ids])); + const remove = (ids) => setSelected(prev => { + for (const id of ids) { + prev.delete(id); + } + return new Set(prev); + }); useEffect(() => { - onChange(selected); + onChange(Array.from(selected)); }, [onChange, selected]); return ( - + {children} );
    + + Epoch + + +
    + +
    + +