diff --git a/clients/admin-ui/cypress/e2e/action-center.cy.ts b/clients/admin-ui/cypress/e2e/action-center.cy.ts index bd12a9563b..de92a17f34 100644 --- a/clients/admin-ui/cypress/e2e/action-center.cy.ts +++ b/clients/admin-ui/cypress/e2e/action-center.cy.ts @@ -182,7 +182,9 @@ describe("Action center", () => { cy.getByTestId("row-0-col-actions").within(() => { cy.getByTestId("ignore-btn").click({ force: true }); }); - cy.wait("@ignoreMonitorResultUncategorizedSystem"); + cy.wait("@ignoreMonitorResultSystem").then((interception) => { + expect(interception.request.url).to.contain("[undefined]"); + }); cy.getByTestId("success-alert").should( "contain", "108 uncategorized assets have been ignored and will not appear in future scans.", @@ -208,8 +210,42 @@ describe("Action center", () => { "10 assets from Google Tag Manager have been ignored and will not appear in future scans.", ); }); + it("shouldn't allow bulk add when uncategorized system is selected", () => { + cy.getByTestId("row-0-col-select").find("label").click(); + cy.getByTestId("selected-count").should("contain", "1 selected"); + cy.getByTestId("bulk-actions-menu").click(); + cy.getByTestId("bulk-add").should("be.disabled"); + }); + it("should bulk add results from categorized systems", () => { + cy.getByTestId("bulk-actions-menu").should("be.disabled"); + cy.getByTestId("row-1-col-select").find("label").click(); + cy.getByTestId("row-2-col-select").find("label").click(); + cy.getByTestId("selected-count").should("contain", "2 selected"); + cy.getByTestId("bulk-actions-menu").should("not.be.disabled"); + cy.getByTestId("bulk-actions-menu").click(); + cy.getByTestId("bulk-add").click(); + cy.wait("@addMonitorResultSystem"); + cy.getByTestId("success-alert").should( + "contain", + "16 assets have been added to the system inventory.", + ); + }); + it("should bulk ignore results from all systems", () => { + cy.getByTestId("row-0-col-select").find("label").click(); + cy.getByTestId("row-1-col-select").find("label").click(); + cy.getByTestId("row-2-col-select").find("label").click(); + cy.getByTestId("selected-count").should("contain", "3 selected"); + cy.getByTestId("bulk-actions-menu").should("not.be.disabled"); + cy.getByTestId("bulk-actions-menu").click(); + cy.getByTestId("bulk-ignore").click(); + cy.wait("@ignoreMonitorResultSystem"); + cy.getByTestId("success-alert").should( + "contain", + "124 assets have been ignored and will not appear in future scans.", + ); + }); it("should navigate to table view on row click", () => { - cy.getByTestId("row-1").click(); + cy.getByTestId("row-1-col-system_name").click(); cy.url().should( "contain", "system_key-8fe42cdb-af2e-4b9e-9b38-f75673180b88", diff --git a/clients/admin-ui/cypress/support/stubs.ts b/clients/admin-ui/cypress/support/stubs.ts index fa175154a7..f4e291add9 100644 --- a/clients/admin-ui/cypress/support/stubs.ts +++ b/clients/admin-ui/cypress/support/stubs.ts @@ -551,13 +551,6 @@ export const stubActionCenter = () => { cy.intercept("POST", "/api/v1/plus/discovery-monitor/*/mute*", { response: 200, }).as("ignoreMonitorResultSystem"); - cy.intercept( - "POST", - "/api/v1/plus/discovery-monitor/*/mute?resolved_system_id=%5Bundefined%5D", - { - response: 200, - }, - ).as("ignoreMonitorResultUncategorizedSystem"); cy.intercept("POST", "/api/v1/plus/discovery-monitor/*/promote*", { response: 200, }).as("addMonitorResultSystem"); diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/action-center.slice.ts b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/action-center.slice.ts index 56afcb59fb..185a493e22 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/action-center.slice.ts +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/action-center.slice.ts @@ -1,4 +1,5 @@ import { baseApi } from "~/features/common/api.slice"; +import { getQueryParamsFromArray } from "~/features/common/utils"; import { PaginationQueryParams } from "~/types/common/PaginationQueryParams"; import { @@ -8,6 +9,11 @@ import { MonitorSystemAggregatePaginatedResponse, } from "./types"; +interface MonitorResultSystemQueryParams { + monitor_config_key: string; + resolved_system_ids: string[]; +} + const actionCenterApi = baseApi.injectEndpoints({ endpoints: (build) => ({ getAggregateMonitorResults: build.query< @@ -57,30 +63,36 @@ const actionCenterApi = baseApi.injectEndpoints({ }), providesTags: () => ["Discovery Monitor Results"], }), - addMonitorResultSystem: build.mutation< + addMonitorResultSystems: build.mutation< any, - { monitor_config_key?: string; resolved_system_id?: string } + MonitorResultSystemQueryParams >({ - query: (params) => ({ - method: "POST", - url: `/plus/discovery-monitor/${params.monitor_config_key}/promote`, - params: { - resolved_system_id: params.resolved_system_id, - }, - }), + query: ({ monitor_config_key, resolved_system_ids }) => { + const params = getQueryParamsFromArray( + resolved_system_ids, + "resolved_system_ids", + ); + return { + method: "POST", + url: `/plus/discovery-monitor/${monitor_config_key}/promote?${params}`, + }; + }, invalidatesTags: ["Discovery Monitor Results"], }), - ignoreMonitorResultSystem: build.mutation< + ignoreMonitorResultSystems: build.mutation< any, - { monitor_config_key?: string; resolved_system_id?: string } + MonitorResultSystemQueryParams >({ - query: (params) => ({ - method: "POST", - url: `/plus/discovery-monitor/${params.monitor_config_key}/mute`, - params: { - resolved_system_id: params.resolved_system_id, - }, - }), + query: ({ monitor_config_key, resolved_system_ids }) => { + const params = getQueryParamsFromArray( + resolved_system_ids, + "resolved_system_ids", + ); + return { + method: "POST", + url: `/plus/discovery-monitor/${monitor_config_key}/mute?${params}`, + }; + }, invalidatesTags: ["Discovery Monitor Results"], }), addMonitorResultAssets: build.mutation({ @@ -163,8 +175,8 @@ export const { useGetAggregateMonitorResultsQuery, useGetDiscoveredSystemAggregateQuery, useGetDiscoveredAssetsQuery, - useAddMonitorResultSystemMutation, - useIgnoreMonitorResultSystemMutation, + useAddMonitorResultSystemsMutation, + useIgnoreMonitorResultSystemsMutation, useAddMonitorResultAssetsMutation, useIgnoreMonitorResultAssetsMutation, useUpdateAssetsSystemMutation, diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/hooks/useDiscoveredSystemAggregateColumns.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/hooks/useDiscoveredSystemAggregateColumns.tsx index 80bed3f415..533bfdd0bc 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/hooks/useDiscoveredSystemAggregateColumns.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/hooks/useDiscoveredSystemAggregateColumns.tsx @@ -1,6 +1,9 @@ import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; -import { DefaultCell } from "~/features/common/table/v2"; +import { + DefaultCell, + IndeterminateCheckboxCell, +} from "~/features/common/table/v2"; import DiscoveredSystemDataUseCell from "~/features/data-discovery-and-detection/action-center/tables/cells/DiscoveredSystemDataUseCell"; import { DiscoveredSystemActionsCell } from "../tables/cells/DiscoveredSystemAggregateActionsCell"; @@ -11,6 +14,28 @@ export const useDiscoveredSystemAggregateColumns = (monitorId: string) => { const columnHelper = createColumnHelper(); const columns: ColumnDef[] = [ + columnHelper.display({ + id: "select", + cell: ({ row }) => ( + + ), + header: ({ table }) => ( + + ), + maxSize: 40, + meta: { + disableRowClick: true, + }, + }), columnHelper.accessor((row) => row.name, { id: "system_name", cell: (props) => ( diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredAssetsTable.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredAssetsTable.tsx index 873ac83199..5bc9952070 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredAssetsTable.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredAssetsTable.tsx @@ -37,7 +37,7 @@ import { } from "~/features/common/table/v2"; import { useAddMonitorResultAssetsMutation, - useAddMonitorResultSystemMutation, + useAddMonitorResultSystemsMutation, useGetDiscoveredAssetsQuery, useIgnoreMonitorResultAssetsMutation, useUpdateAssetsMutation, @@ -71,8 +71,8 @@ export const DiscoveredAssetsTable = ({ useAddMonitorResultAssetsMutation(); const [ignoreMonitorResultAssetsMutation, { isLoading: isIgnoringResults }] = useIgnoreMonitorResultAssetsMutation(); - const [addMonitorResultSystemMutation, { isLoading: isAddingAllResults }] = - useAddMonitorResultSystemMutation(); + const [addMonitorResultSystemsMutation, { isLoading: isAddingAllResults }] = + useAddMonitorResultSystemsMutation(); const [updateAssetsSystemMutation, { isLoading: isBulkUpdatingSystem }] = useUpdateAssetsSystemMutation(); const [updateAssetsMutation, { isLoading: isBulkAddingDataUses }] = @@ -224,9 +224,9 @@ export const DiscoveredAssetsTable = ({ const handleAddAll = async () => { const assetCount = data?.items.length || 0; - const result = await addMonitorResultSystemMutation({ + const result = await addMonitorResultSystemsMutation({ monitor_config_key: monitorId, - resolved_system_id: systemId, + resolved_system_ids: [systemId], }); if (isErrorResult(result)) { diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredSystemAggregateTable.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredSystemAggregateTable.tsx index 2d30b12c00..3b4109f56c 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredSystemAggregateTable.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/action-center/tables/DiscoveredSystemAggregateTable.tsx @@ -1,8 +1,26 @@ -import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; -import { AntEmpty as Empty, Box, Flex } from "fidesui"; +import { + getCoreRowModel, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; +import { + AntButton as Button, + AntEmpty as Empty, + AntTooltip as Tooltip, + Box, + Flex, + Icons, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, +} from "fidesui"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; +import { getErrorMessage } from "~/features/common/helpers"; +import { useAlert } from "~/features/common/hooks"; import { ACTION_CENTER_ROUTE, UNCATEGORIZED_SEGMENT, @@ -14,7 +32,12 @@ import { TableSkeletonLoader, useServerSidePagination, } from "~/features/common/table/v2"; -import { useGetDiscoveredSystemAggregateQuery } from "~/features/data-discovery-and-detection/action-center/action-center.slice"; +import { + useAddMonitorResultSystemsMutation, + useGetDiscoveredSystemAggregateQuery, + useIgnoreMonitorResultSystemsMutation, +} from "~/features/data-discovery-and-detection/action-center/action-center.slice"; +import { isErrorResult } from "~/types/errors"; import { SearchInput } from "../../SearchInput"; import { useDiscoveredSystemAggregateColumns } from "../hooks/useDiscoveredSystemAggregateColumns"; @@ -42,7 +65,18 @@ export const DiscoveredSystemAggregateTable = ({ setTotalPages, resetPageIndexToDefault, } = useServerSidePagination(); + + const [addMonitorResultSystemsMutation, { isLoading: isAddingResults }] = + useAddMonitorResultSystemsMutation(); + const [ignoreMonitorResultSystemsMutation, { isLoading: isIgnoringResults }] = + useIgnoreMonitorResultSystemsMutation(); + + const anyBulkActionIsLoading = isAddingResults || isIgnoringResults; + + const { successAlert, errorAlert } = useAlert(); + const [searchQuery, setSearchQuery] = useState(""); + const [rowSelection, setRowSelection] = useState({}); useEffect(() => { resetPageIndexToDefault(); @@ -69,8 +103,18 @@ export const DiscoveredSystemAggregateTable = ({ manualPagination: true, data: data?.items || [], columnResizeMode: "onChange", + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, }); + const selectedRows = tableInstance.getSelectedRowModel().rows; + + const uncategorizedIsSelected = selectedRows.some( + (row) => row.original.id === null, + ); + if (isLoading) { return ; } @@ -81,6 +125,50 @@ export const DiscoveredSystemAggregateTable = ({ ); }; + const handleBulkAdd = async () => { + const totalUpdates = selectedRows.reduce( + (acc, row) => acc + row.original.total_updates, + 0, + ); + + const result = await addMonitorResultSystemsMutation({ + monitor_config_key: monitorId, + resolved_system_ids: selectedRows.map((row) => row.original.id), + }); + + if (isErrorResult(result)) { + errorAlert(getErrorMessage(result.error)); + } else { + successAlert( + `${totalUpdates} assets have been added to the system inventory.`, + ); + setRowSelection({}); + } + }; + + const handleBulkIgnore = async () => { + const totalUpdates = selectedRows.reduce( + (acc, row) => acc + row.original.total_updates, + 0, + ); + + const result = await ignoreMonitorResultSystemsMutation({ + monitor_config_key: monitorId, + resolved_system_ids: selectedRows.map( + (row) => row.original.id ?? UNCATEGORIZED_SEGMENT, + ), + }); + + if (isErrorResult(result)) { + errorAlert(getErrorMessage(result.error)); + } else { + successAlert( + `${totalUpdates} assets have been ignored and will not appear in future scans.`, + ); + setRowSelection({}); + } + }; + return ( <> @@ -95,6 +183,59 @@ export const DiscoveredSystemAggregateTable = ({ + + {!!selectedRows.length && ( + + {`${selectedRows.length} selected`} + + )} + + } + iconPosition="end" + loading={anyBulkActionIsLoading} + data-testid="bulk-actions-menu" + disabled={!selectedRows.length} + // @ts-ignore - `type` prop is for Ant button, not Chakra MenuButton + type="primary" + > + Actions + + + + + Add + + + + Ignore + + + + { - const [addMonitorResultSystemMutation, { isLoading: isAddingResults }] = - useAddMonitorResultSystemMutation(); - const [ignoreMonitorResultSystemMutation, { isLoading: isIgnoringResults }] = - useIgnoreMonitorResultSystemMutation(); + const [addMonitorResultSystemsMutation, { isLoading: isAddingResults }] = + useAddMonitorResultSystemsMutation(); + const [ignoreMonitorResultSystemsMutation, { isLoading: isIgnoringResults }] = + useIgnoreMonitorResultSystemsMutation(); const { successAlert, errorAlert } = useAlert(); @@ -40,16 +40,16 @@ export const DiscoveredSystemActionsCell = ({ } = system; const handleAdd = async () => { - const result = await addMonitorResultSystemMutation({ + const result = await addMonitorResultSystemsMutation({ monitor_config_key: monitorId, - resolved_system_id: resolvedSystemId, + resolved_system_ids: [resolvedSystemId], }); if (isErrorResult(result)) { errorAlert(getErrorMessage(result.error)); } else { successAlert( !systemKey - ? `${systemName} and ${totalUpdates}assets have been added to the system inventory. ${systemName} is now configured for consent.` + ? `${systemName} and ${totalUpdates} assets have been added to the system inventory. ${systemName} is now configured for consent.` : `${totalUpdates} assets from ${systemName} have been added to the system inventory.`, `Confirmed`, ); @@ -57,9 +57,9 @@ export const DiscoveredSystemActionsCell = ({ }; const handleIgnore = async () => { - const result = await ignoreMonitorResultSystemMutation({ + const result = await ignoreMonitorResultSystemsMutation({ monitor_config_key: monitorId, - resolved_system_id: resolvedSystemId || UNCATEGORIZED_SEGMENT, + resolved_system_ids: [resolvedSystemId || UNCATEGORIZED_SEGMENT], }); if (isErrorResult(result)) { errorAlert(getErrorMessage(result.error));