diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e60f78e8e..b7d35e1ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The types of changes are: * Bulk select and reprocess DSRs that have errored [#1205](https://github.com/ethyca/fides/pull/1489) * Config Wizard: AWS scan results populate in system review forms. [#1454](https://github.com/ethyca/fides/pull/1454) * Integrate rate limiter with Saas Connectors. [#1433](https://github.com/ethyca/fides/pull/1433) +* Added a column selector to the scan results page of the config wizard [#1590](https://github.com/ethyca/fides/pull/1590) ### Changed * Updated mypy to version 0.981 and Python to version 3.10.7 [#1448](https://github.com/ethyca/fides/pull/1448) diff --git a/clients/admin-ui/cypress/e2e/config-wizard.cy.ts b/clients/admin-ui/cypress/e2e/config-wizard.cy.ts index ac22cb385e..8bb3ace6f1 100644 --- a/clients/admin-ui/cypress/e2e/config-wizard.cy.ts +++ b/clients/admin-ui/cypress/e2e/config-wizard.cy.ts @@ -61,10 +61,8 @@ describe.skip("Config Wizard", () => { cy.getByTestId("submit-btn").click(); cy.wait("@postGenerate"); - cy.getByTestId("scan-results-form"); - cy.getByTestId(`scan-result-row-example-system-1`).within(() => { - cy.getByTestId("checkbox").click(); - }); + cy.getByTestId("scan-results"); + cy.getByTestId(`checkbox-example-system-1`).click(); cy.getByTestId("register-btn").click(); // The request while editing the form should match the generated system's body. diff --git a/clients/admin-ui/src/features/dataset/ColumnDropdown.tsx b/clients/admin-ui/src/features/common/ColumnDropdown.tsx similarity index 79% rename from clients/admin-ui/src/features/dataset/ColumnDropdown.tsx rename to clients/admin-ui/src/features/common/ColumnDropdown.tsx index dde9843a5f..e50ea8f491 100644 --- a/clients/admin-ui/src/features/dataset/ColumnDropdown.tsx +++ b/clients/admin-ui/src/features/common/ColumnDropdown.tsx @@ -8,30 +8,42 @@ import { MenuList, Stack, } from "@fidesui/react"; -import { useMemo } from "react"; +import React, { useMemo } from "react"; import { ArrowDownLineIcon } from "~/features/common/Icon"; -import { ColumnMetadata } from "./types"; +export interface ColumnMetadata> { + name: string; + attribute: keyof T; +} -interface Props { - allColumns: ColumnMetadata[]; - selectedColumns: ColumnMetadata[]; - onChange: (columns: ColumnMetadata[]) => void; +interface Props { + allColumns: ColumnMetadata[]; + selectedColumns: ColumnMetadata[]; + onChange: (columns: ColumnMetadata[]) => void; } -const ColumnDropdown = ({ allColumns, selectedColumns, onChange }: Props) => { +export const ColumnDropdown = >({ + allColumns, + selectedColumns, + onChange, +}: Props) => { const nameToColumnInfo = useMemo(() => { const info = new Map(); - allColumns.forEach((c) => info.set(c.name, true)); + allColumns.forEach((c) => + info.set( + c.name, + !!selectedColumns.find((selected) => selected.name === c.name) + ) + ); return info; - }, [allColumns]); + }, [allColumns, selectedColumns]); const handleClear = () => { nameToColumnInfo.forEach((value, key) => nameToColumnInfo.set(key, false)); onChange([]); }; - const handleChange = (column: ColumnMetadata) => { + const handleChange = (column: ColumnMetadata) => { // Toggle the column const prevInfo = nameToColumnInfo.get(column.name) ?? false; nameToColumnInfo.set(column.name, !prevInfo); diff --git a/clients/admin-ui/src/features/common/SystemsCheckboxTable.tsx b/clients/admin-ui/src/features/common/SystemsCheckboxTable.tsx new file mode 100644 index 0000000000..a4bf7348d9 --- /dev/null +++ b/clients/admin-ui/src/features/common/SystemsCheckboxTable.tsx @@ -0,0 +1,149 @@ +import { + Box, + Checkbox, + Table, + TableHeadProps, + Tbody, + Td, + Text, + Th, + Thead, + Tr, +} from "@fidesui/react"; + +import type { ColumnMetadata } from "~/features/common/ColumnDropdown"; +import { System } from "~/types/api"; + +/** + * Index into an object with possibility of nesting + * + * Ex: + * obj = { + * a : { + * b: 'hi' + * } + * } + * resolvePath(obj, 'a') --> { b: 'hi' } + * resolvePath(obj, 'a.b') --> 'hi' + * + * @param object The object to index into + * @param path String path to use as a key + * @returns + */ +export const resolvePath = (object: Record, path: string) => + path.split(".").reduce((o, p) => (o ? o[p] : undefined), object); + +// This component is used within a Chakra Td element. Chakra requires a +// JSX.Element in that context, so all returns in this component need to be wrapped in a fragment. +/* eslint-disable react/jsx-no-useless-fragment */ +const SystemTableCell = ({ + system, + attribute, +}: { + system: System; + attribute: string; +}) => { + if (attribute === "name") { + return ( + + ); + } + if (attribute === "fidesctl_meta.resource_id") { + return ( + + {system.fidesctl_meta?.resource_id} + + ); + } + return <>{resolvePath(system, attribute)}; +}; + +interface Props { + onChange: (systems: System[]) => void; + allSystems: System[]; + checked: System[]; + columns: ColumnMetadata[]; + tableHeadProps?: TableHeadProps; +} +export const SystemsCheckboxTable = ({ + allSystems, + checked, + onChange, + columns, + tableHeadProps, +}: Props) => { + const handleChangeAll = (event: React.ChangeEvent) => { + if (event.target.checked) { + onChange(allSystems); + } else { + onChange([]); + } + }; + const onCheck = (system: System) => { + const exists = checked.indexOf(system) >= 0; + if (!exists) { + onChange([...checked, system]); + } else { + onChange(checked.filter((c) => c.fides_key !== system.fides_key)); + } + }; + + const allChecked = allSystems.length === checked.length; + + if (columns.length === 0) { + return No columns selected to display; + } + + return ( + + + + + {columns.map((c) => ( + + ))} + + + + {allSystems.map((system) => ( + + + {columns.map((c) => ( + + ))} + + ))} + +
+ + {c.name}
+ = 0} + onChange={() => onCheck(system)} + data-testid={`checkbox-${system.fides_key}`} + /> + + +
+ ); +}; + +export default SystemsCheckboxTable; diff --git a/clients/admin-ui/src/features/config-wizard/ConfigWizardWalkthrough.tsx b/clients/admin-ui/src/features/config-wizard/ConfigWizardWalkthrough.tsx index 6ae1943e1d..36bda9b92e 100644 --- a/clients/admin-ui/src/features/config-wizard/ConfigWizardWalkthrough.tsx +++ b/clients/admin-ui/src/features/config-wizard/ConfigWizardWalkthrough.tsx @@ -58,7 +58,7 @@ const ConfigWizardWalkthrough = () => { - + { steps={STEPS} /> - {step === 1 ? : null} - {step === 2 ? : null} - {step === 3 ? : null} - {step === 4 ? : null} - {step === 5 ? ( - - {reviewStep <= 3 ? ( - - ) : null} - {reviewStep === 1 && ( - - )} - {reviewStep === 2 && system && ( - - )} - {reviewStep === 3 && system && ( - dispatch(changeReviewStep())} - abridged - /> - )} - {reviewStep === 4 && system && ( - { - dispatch(reviewManualSystem()); - }} - /> - )} - - ) : null} + + {step === 1 ? : null} + {step === 2 ? : null} + {step === 3 ? : null} + {step === 4 ? ( + + + + ) : null} + {step === 5 ? ( + + {reviewStep <= 3 ? ( + + ) : null} + {reviewStep === 1 && ( + + )} + {reviewStep === 2 && system && ( + + )} + {reviewStep === 3 && system && ( + dispatch(changeReviewStep())} + abridged + /> + )} + {reviewStep === 4 && system && ( + { + dispatch(reviewManualSystem()); + }} + /> + )} + + ) : null} + diff --git a/clients/admin-ui/src/features/config-wizard/ScanResultsForm.tsx b/clients/admin-ui/src/features/config-wizard/ScanResultsForm.tsx index b32bfe1f8a..d63d891f9a 100644 --- a/clients/admin-ui/src/features/config-wizard/ScanResultsForm.tsx +++ b/clients/admin-ui/src/features/config-wizard/ScanResultsForm.tsx @@ -1,74 +1,60 @@ import { Box, Button, - Checkbox, Heading, HStack, Stack, - Table, - TableContainer, - Tbody, - Td, Text, - Th, - Thead, - Tr, useDisclosure, } from "@fidesui/react"; -import { Field, FieldProps, Form, Formik } from "formik"; -import React, { useMemo, useState } from "react"; -import * as Yup from "yup"; +import React, { useState } from "react"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; +import { + ColumnDropdown, + ColumnMetadata, +} from "~/features/common/ColumnDropdown"; +import { SystemsCheckboxTable } from "~/features/common/SystemsCheckboxTable"; +import WarningModal from "~/features/common/WarningModal"; +import { System } from "~/types/api"; -import WarningModal from "../common/WarningModal"; import { changeStep, chooseSystemsForReview, selectSystemsForReview, } from "./config-wizard.slice"; -interface FormValues { - selectedKeys: string[]; -} - -const ValidationSchema = Yup.object().shape({ - selectedKeys: Yup.array() - .of(Yup.string()) - .compact() - .required() - .min(1) - .label("Selected systems"), -}); +const ALL_COLUMNS: ColumnMetadata[] = [ + { name: "Name", attribute: "name" }, + { name: "System type", attribute: "system_type" }, + { name: "Resource ID", attribute: "fidesctl_meta.resource_id" }, +]; const ScanResultsForm = () => { const systems = useAppSelector(selectSystemsForReview); const dispatch = useAppDispatch(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const [selectedKeyValues, setSelectedKeyValues] = useState([]); + const { + isOpen: isWarningOpen, + onOpen: onWarningOpen, + onClose: onWarningClose, + } = useDisclosure(); + const [selectedSystems, setSelectedSystems] = useState(systems); + const [selectedColumns, setSelectedColumns] = + useState(ALL_COLUMNS); - const initialValues: FormValues = useMemo( - () => ({ - selectedKeys: systems.map((s) => s.fides_key), - }), - [systems] - ); + const confirmRegisterSelectedSystems = () => { + dispatch(chooseSystemsForReview(selectedSystems.map((s) => s.fides_key))); + dispatch(changeStep()); + }; - const handleSubmit = (values: FormValues) => { - if (systems.length > values.selectedKeys.length) { - setSelectedKeyValues(values.selectedKeys); - onOpen(); + const handleSubmit = () => { + if (systems.length > selectedSystems.length) { + onWarningOpen(); } else { - dispatch(chooseSystemsForReview(values.selectedKeys)); - dispatch(changeStep()); + confirmRegisterSelectedSystems(); } }; - const confirmRegisterSelectedSystems = () => { - dispatch(chooseSystemsForReview(selectedKeyValues)); - dispatch(changeStep()); - }; - const handleCancel = () => { dispatch(changeStep(2)); }; @@ -78,7 +64,7 @@ const ScanResultsForm = () => { const warningMessage = ( - You’re registering {selectedKeyValues.length} of {systems.length} systems + You’re registering {selectedSystems.length} of {systems.length} systems available. Do you want to continue with registration or cancel and register all systems now? @@ -86,136 +72,52 @@ const ScanResultsForm = () => { return ( - - {({ isValid, values, setValues }) => { - const handleChangeAll = ( - event: React.ChangeEvent - ) => { - if (event.target.checked) { - setValues(initialValues); - } else { - setValues({ selectedKeys: [] }); - } - }; - - const allChecked = - values.selectedKeys.length === initialValues.selectedKeys.length; - - return ( -
- - - Scan results - - - - Below are search results for {region}. Please select and - register the systems you would like to maintain in your - mapping and reports. - - - {/* TODO(#879): Build out the SystemsTable to have a reusable view for selecting - systems, searching by column, etc. - */} - - - - - - - - - - {/* TODO(#876): These fields are not yet returned by the API. - - - - */} - - - - {systems.map((system) => ( - - - - - - - {/* TODO(#876): These fields are not yet returned by the API. - - - - */} - - ))} - -
- - SYSTEM NAMESYSTEM TYPERESOURCE IDREGION/ZONEINSTANCE NAMERESOURCE GROUP
- - {({ field }: FieldProps) => ( - - )} - - - - {system.system_type} - - {system.fidesctl_meta?.resource_id} - - {system.fidesctl_meta?.region_name}{system.fidesctl_meta?.instance_name}{system.fidesctl_meta?.resource_group}
-
+ + + Scan results + + + + + Below are search results for {region}. Please select and register + the systems you would like to maintain in your mapping and reports. + + + + + + + + + + + + - - - - -
-
- ); - }} -
); diff --git a/clients/admin-ui/src/features/config-wizard/SuccessPage.tsx b/clients/admin-ui/src/features/config-wizard/SuccessPage.tsx index 6f1222bf8c..639075c578 100644 --- a/clients/admin-ui/src/features/config-wizard/SuccessPage.tsx +++ b/clients/admin-ui/src/features/config-wizard/SuccessPage.tsx @@ -85,6 +85,7 @@ const SuccessPage = ({ {s.name} diff --git a/clients/admin-ui/src/features/dataset/DatasetCollectionView.tsx b/clients/admin-ui/src/features/dataset/DatasetCollectionView.tsx index 5f09d1a840..f97fa7a537 100644 --- a/clients/admin-ui/src/features/dataset/DatasetCollectionView.tsx +++ b/clients/admin-ui/src/features/dataset/DatasetCollectionView.tsx @@ -2,12 +2,16 @@ import { Box, HStack, Select, Spinner } from "@fidesui/react"; import { ChangeEvent, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { + ColumnDropdown, + ColumnMetadata, +} from "~/features/common/ColumnDropdown"; import { useFeatures } from "~/features/common/features.slice"; import { useGetClassifyDatasetQuery } from "~/features/common/plus.slice"; import { useGetAllDataCategoriesQuery } from "~/features/taxonomy/taxonomy.slice"; +import { DatasetField } from "~/types/api"; import ApproveClassification from "./ApproveClassification"; -import ColumnDropdown from "./ColumnDropdown"; import { selectActiveCollection, selectActiveCollections, @@ -22,9 +26,9 @@ import DatasetHeading from "./DatasetHeading"; import EditCollectionDrawer from "./EditCollectionDrawer"; import EditDatasetDrawer from "./EditDatasetDrawer"; import MoreActionsMenu from "./MoreActionsMenu"; -import { ColumnMetadata, EditableType } from "./types"; +import { EditableType } from "./types"; -const ALL_COLUMNS: ColumnMetadata[] = [ +const ALL_COLUMNS: ColumnMetadata[] = [ { name: "Field Name", attribute: "name" }, { name: "Description", attribute: "description" }, { name: "Identifiability", attribute: "data_qualifier" }, @@ -63,7 +67,8 @@ const DatasetCollectionView = ({ fidesKey }: Props) => { const activeCollection = useSelector(selectActiveCollection); const activeEditor = useSelector(selectActiveEditor); - const [columns, setColumns] = useState(ALL_COLUMNS); + const [columns, setColumns] = + useState[]>(ALL_COLUMNS); // Query subscriptions: useGetAllDataCategoriesQuery(); diff --git a/clients/admin-ui/src/features/dataset/DatasetFieldsTable.tsx b/clients/admin-ui/src/features/dataset/DatasetFieldsTable.tsx index a563f328b7..3b07de31cf 100644 --- a/clients/admin-ui/src/features/dataset/DatasetFieldsTable.tsx +++ b/clients/admin-ui/src/features/dataset/DatasetFieldsTable.tsx @@ -1,7 +1,9 @@ import { Box, Table, Tbody, Td, Th, Thead, Tr } from "@fidesui/react"; import { useDispatch, useSelector } from "react-redux"; +import { ColumnMetadata } from "~/features/common/ColumnDropdown"; import { selectClassifyInstanceFieldMap } from "~/features/common/plus.slice"; +import { DatasetField } from "~/types/api"; import { selectActiveEditor, @@ -12,10 +14,10 @@ import { } from "./dataset.slice"; import DatasetFieldCell from "./DatasetFieldCell"; import EditFieldDrawer from "./EditFieldDrawer"; -import { ColumnMetadata, EditableType } from "./types"; +import { EditableType } from "./types"; interface Props { - columns: ColumnMetadata[]; + columns: ColumnMetadata[]; } const DatasetFieldsTable = ({ columns }: Props) => { diff --git a/clients/admin-ui/src/features/dataset/types.ts b/clients/admin-ui/src/features/dataset/types.ts index 7988a92c08..ca473fae37 100644 --- a/clients/admin-ui/src/features/dataset/types.ts +++ b/clients/admin-ui/src/features/dataset/types.ts @@ -1,9 +1,4 @@ -import { DataCategory, DatasetField } from "~/types/api"; - -export interface ColumnMetadata { - name: string; - attribute: keyof DatasetField; -} +import { DataCategory } from "~/types/api"; export enum EditableType { DATASET = "dataset",