diff --git a/CHANGELOG.md b/CHANGELOG.md index ead22a814a..c9ae87e10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o ### Added - Added Action Center MVP behind new feature flag [#5622](https://github.com/ethyca/fides/pull/5622) +- Added Data Catalog MVP behind new feature flag [#5628](https://github.com/ethyca/fides/pull/5628) - Added cache-clearing methods to the `DBCache` model to allow deleting cache entries [#5629](https://github.com/ethyca/fides/pull/5629) - Adds partitioning, custom identities, multiple identities to test coverage for BigQuery Enterprise [#5618](https://github.com/ethyca/fides/pull/5618) - Added Datahub groundwork required by Fidesplus [#5666](https://github.com/ethyca/fides/pull/5666) @@ -196,6 +197,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o ### Fixed - API router sanitizer being too aggressive with NextJS Catch-all Segments [#5438](https://github.com/ethyca/fides/pull/5438) + - Fix rendering of subfield names in D&D tables [#5439](https://github.com/ethyca/fides/pull/5439) - Fix BigQuery `partitioning` queries to properly support multiple identity clauses [#5432](https://github.com/ethyca/fides/pull/5432) ## [2.48.0](https://github.com/ethyca/fides/compare/2.47.1...2.48.0) diff --git a/clients/admin-ui/cypress/e2e/data-catalog.cy.ts b/clients/admin-ui/cypress/e2e/data-catalog.cy.ts new file mode 100644 index 0000000000..2eda2079b4 --- /dev/null +++ b/clients/admin-ui/cypress/e2e/data-catalog.cy.ts @@ -0,0 +1,133 @@ +import { + stubDataCatalog, + stubPlus, + stubStagedResourceActions, + stubSystemCrud, + stubTaxonomyEntities, +} from "cypress/support/stubs"; + +import { DATA_CATALOG_ROUTE } from "~/features/common/nav/v2/routes"; + +describe("data catalog", () => { + beforeEach(() => { + cy.login(); + stubPlus(true); + stubDataCatalog(); + stubTaxonomyEntities(); + stubSystemCrud(); + }); + + describe("systems table", () => { + beforeEach(() => { + cy.visit(DATA_CATALOG_ROUTE); + cy.wait("@getCatalogSystems"); + }); + + it("should display systems table", () => { + cy.getByTestId("row-bigquery_system-col-name").should( + "contain", + "BigQuery System", + ); + }); + + it("should be able to navigate to system details via the overflow menu", () => { + cy.getByTestId("row-bigquery_system").within(() => { + cy.getByTestId("system-actions-menu").click(); + cy.getByTestId("view-system-details").click({ force: true }); + cy.url().should("include", "/systems/configure/bigquery_system"); + }); + }); + + it("should be able to add a data use", () => { + cy.getByTestId("row-bigquery_system-col-data-uses").within(() => { + cy.getByTestId("taxonomy-add-btn").click(); + cy.get(".select-wrapper").should("be.visible"); + }); + }); + + it("should navigate to database view when clicking a system with projects", () => { + cy.getByTestId("row-bigquery_system-col-name").click(); + cy.wait("@getAvailableDatabases"); + cy.url().should("include", "/bigquery_system/projects"); + }); + + it("should navigate to dataset view when clicking a system without projects", () => { + cy.intercept("POST", "/api/v1/plus/discovery-monitor/databases*", { + fixture: "empty-pagination", + }).as("getEmptyAvailableDatabases"); + cy.getByTestId("row-dynamo_system-col-name").click(); + cy.wait("@getEmptyAvailableDatabases"); + cy.url().should("not.include", "/projects"); + }); + }); + + describe("projects table", () => { + beforeEach(() => { + cy.visit(`${DATA_CATALOG_ROUTE}/bigquery_system/projects`); + cy.wait("@getCatalogProjects"); + }); + + it("should show projects with appropriate statuses", () => { + cy.getByTestId( + "row-bigquery_monitor.prj-bigquery-111111-col-status", + ).should("contain", "Attention required"); + cy.getByTestId( + "row-bigquery_monitor.prj-bigquery-222222-col-status", + ).should("contain", "Classifying"); + cy.getByTestId( + "row-bigquery_monitor.prj-bigquery-333333-col-status", + ).should("contain", "In review"); + cy.getByTestId( + "row-bigquery_monitor.prj-bigquery-444444-col-status", + ).should("contain", "Approved"); + }); + + it("should navigate to dataset view on click", () => { + cy.getByTestId( + "row-bigquery_monitor.prj-bigquery-111111-col-name", + ).click(); + cy.url().should( + "include", + "/projects/bigquery_monitor.prj-bigquery-111111", + ); + }); + }); + + describe("resource tables", () => { + beforeEach(() => { + stubStagedResourceActions(); + cy.visit( + `${DATA_CATALOG_ROUTE}/bigquery_system/monitor.project.test_dataset_1`, + ); + }); + + it("should display the table", () => { + cy.getByTestId("row-monitor.project.dataset.table_1-col-name").should( + "contain", + "table_1", + ); + }); + + it("should be able to take actions on resources", () => { + cy.getByTestId("row-monitor.project.dataset.table_1-col-actions").within( + () => { + cy.getByTestId("classify-btn").click(); + cy.wait("@confirmResource"); + }, + ); + cy.getByTestId("row-monitor.project.dataset.table_2-col-actions").within( + () => { + cy.getByTestId("resource-actions-menu").click(); + cy.getByTestId("hide-action").click({ force: true }); + cy.wait("@ignoreResource"); + }, + ); + cy.getByTestId("row-monitor.project.dataset.table_3-col-actions").within( + () => { + cy.getByTestId("approve-btn").click(); + cy.wait("@promoteResource"); + }, + ); + }); + }); +}); diff --git a/clients/admin-ui/cypress/e2e/discovery-detection.cy.ts b/clients/admin-ui/cypress/e2e/discovery-detection.cy.ts index b6831c64dd..cb7a3dff5c 100644 --- a/clients/admin-ui/cypress/e2e/discovery-detection.cy.ts +++ b/clients/admin-ui/cypress/e2e/discovery-detection.cy.ts @@ -410,9 +410,7 @@ describe("discovery and detection", () => { cy.intercept("PATCH", "/api/v1/plus/discovery-monitor/*/results").as( "patchClassification", ); - cy.getByTestId("classification-user.device.device_id").click({ - force: true, - }); + cy.getByTestId("classification-user.contact.phone_number").click(); cy.getByTestId("taxonomy-select").antSelect("system"); cy.wait("@patchClassification"); }); @@ -424,7 +422,7 @@ describe("discovery and detection", () => { cy.getByTestId( "user-classification-user.contact.phone_number", ).should("exist"); - cy.getByTestId("add-category-btn").click(); + cy.getByTestId("taxonomy-add-btn").click(); cy.get(".select-wrapper").should("exist"); }); }); @@ -434,7 +432,7 @@ describe("discovery and detection", () => { "row-my_bigquery_monitor.prj-bigquery-418515.test_dataset_1.consent-reports-20.No_categories-col-classifications", ).within(() => { cy.getByTestId("no-classifications").should("exist"); - cy.getByTestId("add-category-btn").should("exist"); + cy.getByTestId("taxonomy-add-btn").should("exist"); }); }); @@ -443,7 +441,7 @@ describe("discovery and detection", () => { "row-my_bigquery_monitor.prj-bigquery-418515.test_dataset_1.consent-reports-20.address-col-classifications", ).within(() => { cy.getByTestId("no-classifications").should("exist"); - cy.getByTestId("add-category-btn").should("not.exist"); + cy.getByTestId("taxonomy-add-btn").should("not.exist"); }); }); }); diff --git a/clients/admin-ui/cypress/fixtures/data-catalog/catalog-projects.json b/clients/admin-ui/cypress/fixtures/data-catalog/catalog-projects.json new file mode 100644 index 0000000000..3071598c4d --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/data-catalog/catalog-projects.json @@ -0,0 +1,32 @@ +{ + "items": [ + { + "urn": "bigquery_monitor.prj-bigquery-111111", + "name": "prj-bigquery-111111", + "diff_status": "addition", + "child_diff_status": { "addition": true } + }, + { + "urn": "bigquery_monitor.prj-bigquery-222222", + "name": "prj-bigquery-222222", + "diff_status": "classifying", + "child_diff_status": { "classifying": true } + }, + { + "urn": "bigquery_monitor.prj-bigquery-333333", + "name": "prj-bigquery-333333", + "diff_status": "classification_addition", + "child_diff_status": { "classification_addition": true } + }, + { + "urn": "bigquery_monitor.prj-bigquery-444444", + "name": "prj-bigquery-444444", + "diff_status": "monitored", + "child_diff_status": {} + } + ], + "total": 2, + "page": 1, + "size": 25, + "pages": 1 +} diff --git a/clients/admin-ui/cypress/fixtures/data-catalog/catalog-systems.json b/clients/admin-ui/cypress/fixtures/data-catalog/catalog-systems.json new file mode 100644 index 0000000000..aa53ad57fc --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/data-catalog/catalog-systems.json @@ -0,0 +1,33 @@ +{ + "items": [ + { + "fides_key": "bigquery_system", + "name": "BigQuery System", + "description": "A system used for storing and analyzing large datasets.", + "monitor_config_keys": ["bigquery_monitor"], + "connection_configs": { + "key": "bq_integration" + } + }, + { + "fides_key": "dynamo_system", + "name": "Dynamo System", + "description": "A system used for storing and analyzing large datasets.", + "monitor_config_keys": ["dynamo_monitor"], + "connection_configs": { + "key": "dynamo_integration" + } + }, + { + "fides_key": "system_with_dataset", + "name": "System with Dataset", + "description": "A system with a dataset.", + "monitor_config_keys": [], + "dataset_references": ["demo_dataset"] + } + ], + "total": 3, + "page": 1, + "size": 25, + "pages": 1 +} diff --git a/clients/admin-ui/cypress/fixtures/data-catalog/catalog-tables.json b/clients/admin-ui/cypress/fixtures/data-catalog/catalog-tables.json new file mode 100644 index 0000000000..56ff2e1f2f --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/data-catalog/catalog-tables.json @@ -0,0 +1,28 @@ +{ + "items": [ + { + "urn": "monitor.project.dataset.table_1", + "name": "table_1", + "diff_status": "addition" + }, + { + "urn": "monitor.project.dataset.table_2", + "name": "table_2", + "diff_status": "classifying" + }, + { + "urn": "monitor.project.dataset.table_3", + "name": "table_3", + "diff_status": "classification_addition" + }, + { + "urn": "monitor.project.dataset.table_4", + "name": "table_4", + "diff_status": "monitored" + } + ], + "total": 4, + "page": 1, + "size": 25, + "pages": 1 +} diff --git a/clients/admin-ui/cypress/support/stubs.ts b/clients/admin-ui/cypress/support/stubs.ts index df518408db..34d7d35b4b 100644 --- a/clients/admin-ui/cypress/support/stubs.ts +++ b/clients/admin-ui/cypress/support/stubs.ts @@ -534,3 +534,22 @@ export const stubActionCenter = () => { response: 200, }).as("setAssetSystem"); }; + +export const stubDataCatalog = () => { + cy.intercept("GET", "/api/v1/plus/data-catalog/system*", { + fixture: "data-catalog/catalog-systems", + }).as("getCatalogSystems"); + cy.intercept("GET", "/api/v1/plus/data-catalog/project*", { + fixture: "data-catalog/catalog-projects", + }).as("getCatalogProjects"); + cy.intercept("GET", "/api/v1/plus/discovery-monitor/results?*", { + fixture: "data-catalog/catalog-tables", + }).as("getCatalogTables"); + cy.intercept("POST", "/api/v1/plus/discovery-monitor/databases*", { + items: ["test_project"], + page: 1, + size: 1, + total: 1, + pages: 1, + }).as("getAvailableDatabases"); +}; diff --git a/clients/admin-ui/src/features/common/api.slice.ts b/clients/admin-ui/src/features/common/api.slice.ts index 4a6c5a9f1e..4a96076796 100644 --- a/clients/admin-ui/src/features/common/api.slice.ts +++ b/clients/admin-ui/src/features/common/api.slice.ts @@ -19,6 +19,8 @@ export const baseApi = createApi({ tagTypes: [ "Allow List", "Auth", + "Catalog Systems", + "Catalog Projects", "Classify Instances Datasets", "Classify Instances Systems", "Connection Type", diff --git a/clients/admin-ui/src/features/common/dropdown/DataCategorySelect.tsx b/clients/admin-ui/src/features/common/dropdown/DataCategorySelect.tsx new file mode 100644 index 0000000000..10e76a078f --- /dev/null +++ b/clients/admin-ui/src/features/common/dropdown/DataCategorySelect.tsx @@ -0,0 +1,40 @@ +import { + TaxonomySelect, + TaxonomySelectOption, + TaxonomySelectProps, +} from "~/features/common/dropdown/TaxonomySelect"; +import useTaxonomies from "~/features/common/hooks/useTaxonomies"; + +const DataCategorySelect = ({ + selectedTaxonomies, + showDisabled = false, + ...props +}: TaxonomySelectProps) => { + const { getDataCategoryDisplayNameProps, getDataCategories } = + useTaxonomies(); + + const getActiveDataCategories = () => + getDataCategories().filter((c) => c.active); + + const dataCategories = showDisabled + ? getDataCategories() + : getActiveDataCategories(); + + const options: TaxonomySelectOption[] = dataCategories + .filter((category) => !selectedTaxonomies.includes(category.fides_key)) + .map((category) => { + const { name, primaryName } = getDataCategoryDisplayNameProps( + category.fides_key, + ); + return { + value: category.fides_key, + name, + primaryName, + description: category.description || "", + }; + }); + + return ; +}; + +export default DataCategorySelect; diff --git a/clients/admin-ui/src/features/common/dropdown/DataUseSelect.tsx b/clients/admin-ui/src/features/common/dropdown/DataUseSelect.tsx new file mode 100644 index 0000000000..d61f5aabd6 --- /dev/null +++ b/clients/admin-ui/src/features/common/dropdown/DataUseSelect.tsx @@ -0,0 +1,36 @@ +import { + TaxonomySelect, + TaxonomySelectOption, + TaxonomySelectProps, +} from "~/features/common/dropdown/TaxonomySelect"; +import useTaxonomies from "~/features/common/hooks/useTaxonomies"; + +const DataUseSelect = ({ + selectedTaxonomies, + showDisabled = false, + ...props +}: TaxonomySelectProps) => { + const { getDataUseDisplayNameProps, getDataUses } = useTaxonomies(); + + const getActiveDataUses = () => getDataUses().filter((du) => du.active); + + const dataUses = showDisabled ? getDataUses() : getActiveDataUses(); + + const options: TaxonomySelectOption[] = dataUses + .filter((dataUse) => !selectedTaxonomies.includes(dataUse.fides_key)) + .map((dataUse) => { + const { name, primaryName } = getDataUseDisplayNameProps( + dataUse.fides_key, + ); + return { + value: dataUse.fides_key, + name, + primaryName, + description: dataUse.description || "", + }; + }); + + return ; +}; + +export default DataUseSelect; diff --git a/clients/admin-ui/src/features/common/dropdown/TaxonomySelect.tsx b/clients/admin-ui/src/features/common/dropdown/TaxonomySelect.tsx index ef9450be4c..83a643a01f 100644 --- a/clients/admin-ui/src/features/common/dropdown/TaxonomySelect.tsx +++ b/clients/admin-ui/src/features/common/dropdown/TaxonomySelect.tsx @@ -4,7 +4,6 @@ import { AntSelectProps as SelectProps, } from "fidesui"; -import useTaxonomies from "../hooks/useTaxonomies"; import styles from "./TaxonomySelect.module.scss"; export interface TaxonomySelectOption { @@ -15,7 +14,7 @@ export interface TaxonomySelectOption { className?: string; } -const TaxonomyOption = ({ data }: { data: TaxonomySelectOption }) => { +export const TaxonomyOption = ({ data }: { data: TaxonomySelectOption }) => { return ( { ); }; -interface TaxonomySelectProps - extends SelectProps { +export interface TaxonomySelectProps + extends Omit, "options"> { selectedTaxonomies: string[]; showDisabled?: boolean; } + export const TaxonomySelect = ({ - selectedTaxonomies, - showDisabled = false, + options, ...props -}: TaxonomySelectProps) => { - const { getDataCategoryDisplayNameProps, getDataCategories } = - useTaxonomies(); - - const getActiveDataCategories = () => - getDataCategories().filter((c) => c.active); - - const dataCategories = showDisabled - ? getDataCategories() - : getActiveDataCategories(); - - const options: TaxonomySelectOption[] = dataCategories - .filter((category) => !selectedTaxonomies.includes(category.fides_key)) - .map((category) => { - const { name, primaryName } = getDataCategoryDisplayNameProps( - category.fides_key, - ); - return { - value: category.fides_key, - name, - primaryName, - description: category.description || "", - className: styles.option, - }; - }); +}: SelectProps) => { + const selectOptions = options?.map((opt) => ({ + ...opt, + className: styles.option, + })); return ( + options={selectOptions} autoFocus showSearch variant="borderless" - placeholder="Select a category..." - options={options} optionRender={TaxonomyOption} dropdownStyle={{ minWidth: "500px" }} className="w-full p-0" diff --git a/clients/admin-ui/src/features/common/hooks/useTaxonomies.tsx b/clients/admin-ui/src/features/common/hooks/useTaxonomies.tsx index 15bc018535..7e3659a5f2 100644 --- a/clients/admin-ui/src/features/common/hooks/useTaxonomies.tsx +++ b/clients/admin-ui/src/features/common/hooks/useTaxonomies.tsx @@ -107,6 +107,10 @@ const useTaxonomies = () => { const getDataUseDisplayName = (dataUseKey: string): JSX.Element | string => getDataDisplayName(dataUseKey, getDataUseByKey, 1); + const getDataUseDisplayNameProps = ( + dataUseKey: string, + ): DataDisplayNameProps => + getDataDisplayNameProps(dataUseKey, getDataUseByKey, 1); /* Data Categories @@ -143,6 +147,7 @@ const useTaxonomies = () => { getDataUses, getDataUseByKey, getDataUseDisplayName, + getDataUseDisplayNameProps, getDataCategories, getDataCategoryByKey, getDataCategoryDisplayName, diff --git a/clients/admin-ui/src/features/common/nav/v2/nav-config.ts b/clients/admin-ui/src/features/common/nav/v2/nav-config.ts index ed8ae94d28..13d4b0808b 100644 --- a/clients/admin-ui/src/features/common/nav/v2/nav-config.ts +++ b/clients/admin-ui/src/features/common/nav/v2/nav-config.ts @@ -66,6 +66,13 @@ export const NAV_CONFIG: NavConfigGroup[] = [ requiresFlag: "dataDiscoveryAndDetection", requiresPlus: true, }, + { + title: "Data catalog", + path: routes.DATA_CATALOG_ROUTE, + scopes: [], + requiresFlag: "dataCatalog", + requiresPlus: true, + }, ], }, { diff --git a/clients/admin-ui/src/features/common/nav/v2/routes.ts b/clients/admin-ui/src/features/common/nav/v2/routes.ts index e14dc5ef42..384df79fcd 100644 --- a/clients/admin-ui/src/features/common/nav/v2/routes.ts +++ b/clients/admin-ui/src/features/common/nav/v2/routes.ts @@ -32,6 +32,9 @@ export const DATA_DISCOVERY_ROUTE = "/data-discovery/discovery"; export const DATA_DISCOVERY_ROUTE_DETAIL = "/data-discovery/discovery/[resourceUrn]"; +// End-to-end datasets +export const DATA_CATALOG_ROUTE = "/data-catalog"; + // Privacy requests group export const DATASTORE_CONNECTION_ROUTE = "/datastore-connection"; export const PRIVACY_REQUESTS_ROUTE = "/privacy-requests"; diff --git a/clients/admin-ui/src/features/common/table/v2/FidesCell.tsx b/clients/admin-ui/src/features/common/table/v2/FidesCell.tsx index fe5e7da3d9..50c8a19c41 100644 --- a/clients/admin-ui/src/features/common/table/v2/FidesCell.tsx +++ b/clients/admin-ui/src/features/common/table/v2/FidesCell.tsx @@ -108,6 +108,7 @@ export const FidesCell = ({ height="inherit" onClick={handleCellClick} data-testid={`row-${cell.row.id}-col-${cell.column.id}`} + {...cell.column.columnDef.meta?.cellProps} > {!cell.getIsPlaceholder() || isFirstRowOfGroupedRows ? flexRender(cell.column.columnDef.cell, { diff --git a/clients/admin-ui/src/features/common/table/v2/FidesTable.tsx b/clients/admin-ui/src/features/common/table/v2/FidesTable.tsx index efbbaa5021..97f57ededf 100644 --- a/clients/admin-ui/src/features/common/table/v2/FidesTable.tsx +++ b/clients/admin-ui/src/features/common/table/v2/FidesTable.tsx @@ -22,6 +22,7 @@ import { Portal, SmallCloseIcon, Table, + TableCellProps, TableContainer, Tbody, Td, @@ -57,6 +58,7 @@ declare module "@tanstack/table-core" { showHeaderMenuWrapOption?: boolean; overflow?: "auto" | "visible" | "hidden"; disableRowClick?: boolean; + cellProps?: TableCellProps; noPadding?: boolean; onCellClick?: (row: TData) => void; } @@ -439,6 +441,7 @@ export const FidesTableV2 = ({ opacity: 1, }, }} + {...header.column.columnDef.meta?.cellProps} > void; onRemoveTaxonomy: (taxonomy: string) => void; } -const TaxonomiesPicker = ({ +const TaxonomySelectCell = ({ selectedTaxonomies, onAddTaxonomy, onRemoveTaxonomy, -}: TaxonomiesPickerProps) => { +}: TaxonomyCellProps) => { const [isAdding, setIsAdding] = useState(false); const { getDataCategoryDisplayName } = useTaxonomies(); return ( - + {selectedTaxonomies.map((category) => ( - { setIsAdding(false); @@ -90,7 +82,7 @@ const TaxonomiesPicker = ({ /> )} - + ); }; -export default TaxonomiesPicker; +export default TaxonomySelectCell; diff --git a/clients/admin-ui/src/features/data-catalog/CatalogResourceActionsCell.tsx b/clients/admin-ui/src/features/data-catalog/CatalogResourceActionsCell.tsx new file mode 100644 index 0000000000..982bf07835 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/CatalogResourceActionsCell.tsx @@ -0,0 +1,113 @@ +import { + AntButton, + AntButton as Button, + Flex, + Menu, + MenuButton, + MenuItem, + MenuList, + MoreIcon, +} from "fidesui"; + +import { useAlert } from "~/features/common/hooks"; +import { + CatalogResourceStatus, + getCatalogResourceStatus, +} from "~/features/data-catalog/utils"; +import { + useConfirmResourceMutation, + useMuteResourceMutation, + usePromoteResourceMutation, +} from "~/features/data-discovery-and-detection/discovery-detection.slice"; +import { StagedResourceAPIResponse } from "~/types/api"; + +const CatalogResourceActionsCell = ({ + resource, +}: { + resource: StagedResourceAPIResponse; +}) => { + const { successAlert } = useAlert(); + const status = getCatalogResourceStatus(resource); + const [confirmResource, { isLoading: classifyIsLoading }] = + useConfirmResourceMutation(); + const [promoteResource, { isLoading: approveIsLoading }] = + usePromoteResourceMutation(); + const [muteResource, { isLoading: muteIsLoading }] = + useMuteResourceMutation(); + + const classifyResource = async () => { + await confirmResource({ + staged_resource_urn: resource.urn, + monitor_config_id: resource.monitor_config_id!, + unmute_children: true, + classify_monitored_resources: true, + }); + successAlert( + `Started classification on ${resource.name ?? "this resource"}`, + ); + }; + + const approveResource = async () => { + await promoteResource({ + staged_resource_urn: resource.urn, + }); + successAlert(`Approved ${resource.name ?? " resource"}`); + }; + + const hideResource = async () => { + await muteResource({ + staged_resource_urn: resource.urn, + }); + successAlert(`Hid ${resource.name ?? " resource"}`); + }; + + const anyActionIsLoading = + classifyIsLoading || approveIsLoading || muteIsLoading; + + return ( + + {status === CatalogResourceStatus.ATTENTION_REQUIRED && ( + + )} + {status === CatalogResourceStatus.IN_REVIEW && ( + + )} + + } + data-testid="resource-actions-menu" + /> + + + Hide + + + + + ); +}; + +export default CatalogResourceActionsCell; diff --git a/clients/admin-ui/src/features/data-catalog/CatalogResourceNameCell.tsx b/clients/admin-ui/src/features/data-catalog/CatalogResourceNameCell.tsx new file mode 100644 index 0000000000..ea367a6895 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/CatalogResourceNameCell.tsx @@ -0,0 +1,19 @@ +import { DefaultCell } from "~/features/common/table/v2"; +import getResourceName from "~/features/data-discovery-and-detection/utils/getResourceName"; +import resourceHasChildren from "~/features/data-discovery-and-detection/utils/resourceHasChildren"; +import { StagedResourceAPIResponse } from "~/types/api"; + +const CatalogResourceNameCell = ({ + resource, +}: { + resource: StagedResourceAPIResponse; +}) => { + return ( + + ); +}; + +export default CatalogResourceNameCell; diff --git a/clients/admin-ui/src/features/data-catalog/CatalogStatusBadgeCell.tsx b/clients/admin-ui/src/features/data-catalog/CatalogStatusBadgeCell.tsx new file mode 100644 index 0000000000..ef1cfb4b32 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/CatalogStatusBadgeCell.tsx @@ -0,0 +1,19 @@ +import { BadgeCell } from "~/features/common/table/v2"; +import { CatalogResourceStatus } from "~/features/data-catalog/utils"; + +const STATUS_COLOR_MAP: Record = { + [CatalogResourceStatus.ATTENTION_REQUIRED]: "red", + [CatalogResourceStatus.APPROVED]: "green", + [CatalogResourceStatus.IN_REVIEW]: "yellow", + [CatalogResourceStatus.CLASSIFYING]: "blue", +}; + +const CatalogStatusBadgeCell = ({ + status, +}: { + status: CatalogResourceStatus; +}) => { + return ; +}; + +export default CatalogStatusBadgeCell; diff --git a/clients/admin-ui/src/features/data-catalog/data-catalog.slice.ts b/clients/admin-ui/src/features/data-catalog/data-catalog.slice.ts new file mode 100644 index 0000000000..27d8bd9bcd --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/data-catalog.slice.ts @@ -0,0 +1,72 @@ +import { createSlice } from "@reduxjs/toolkit"; + +import { baseApi } from "~/features/common/api.slice"; +import { + Page_StagedResourceAPIResponse_, + Page_SystemWithMonitorKeys_, +} from "~/types/api"; +import { PaginationQueryParams } from "~/types/common/PaginationQueryParams"; + +const initialState = { + page: 1, + pageSize: 50, +}; + +interface CatalogSystemQueryParams extends PaginationQueryParams { + show_hidden?: boolean; +} + +interface CatalogResourceQueryParams extends PaginationQueryParams { + monitor_config_ids?: string[]; + show_hidden?: boolean; +} + +const dataCatalogApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + getCatalogSystems: build.query< + Page_SystemWithMonitorKeys_, + CatalogSystemQueryParams + >({ + query: (params) => ({ + method: "GET", + url: `/plus/data-catalog/system`, + params, + }), + providesTags: ["Catalog Systems", "System"], + }), + getCatalogProjects: build.query< + Page_StagedResourceAPIResponse_, + CatalogResourceQueryParams + >({ + query: ({ ...params }) => ({ + method: "GET", + url: `/plus/data-catalog/project`, + params, + }), + providesTags: ["Discovery Monitor Results"], + }), + getCatalogDatasets: build.query< + Page_StagedResourceAPIResponse_, + CatalogResourceQueryParams + >({ + query: ({ ...params }) => ({ + method: "GET", + url: `/plus/data-catalog/dataset`, + params, + }), + providesTags: ["Discovery Monitor Results"], + }), + }), +}); + +export const { + useGetCatalogSystemsQuery, + useGetCatalogProjectsQuery, + useGetCatalogDatasetsQuery, +} = dataCatalogApi; + +export const dataCatalogApiSlice = createSlice({ + name: "dataCatalog", + initialState, + reducers: {}, +}); diff --git a/clients/admin-ui/src/features/data-catalog/datasets/EmptyCatalogTableNotice.tsx b/clients/admin-ui/src/features/data-catalog/datasets/EmptyCatalogTableNotice.tsx new file mode 100644 index 0000000000..26c3536e2b --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/datasets/EmptyCatalogTableNotice.tsx @@ -0,0 +1,21 @@ +import { Text, VStack } from "fidesui"; + +const EmptyCatalogTableNotice = () => ( + + + No resources found + + You're up to date! + +); + +export default EmptyCatalogTableNotice; diff --git a/clients/admin-ui/src/features/data-catalog/datasets/useCatalogDatasetColumns.tsx b/clients/admin-ui/src/features/data-catalog/datasets/useCatalogDatasetColumns.tsx new file mode 100644 index 0000000000..11ecbc09cd --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/datasets/useCatalogDatasetColumns.tsx @@ -0,0 +1,51 @@ +/* eslint-disable react/no-unstable-nested-components */ + +import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; + +import { DefaultCell } from "~/features/common/table/v2"; +import { RelativeTimestampCell } from "~/features/common/table/v2/cells"; +import CatalogResourceNameCell from "~/features/data-catalog/CatalogResourceNameCell"; +import CatalogStatusBadgeCell from "~/features/data-catalog/CatalogStatusBadgeCell"; +import { getCatalogResourceStatus } from "~/features/data-catalog/utils"; +import { StagedResourceAPIResponse } from "~/types/api"; + +const columnHelper = createColumnHelper(); + +const useCatalogDatasetColumns = () => { + const columns: ColumnDef[] = useMemo( + () => [ + columnHelper.accessor((row) => row.name, { + id: "name", + cell: (props) => ( + + ), + header: "Dataset", + }), + columnHelper.display({ + id: "status", + cell: ({ row }) => ( + + ), + header: "Status", + }), + columnHelper.accessor((row) => row.description, { + id: "description", + cell: (props) => , + header: "Description", + }), + columnHelper.accessor((row) => row.updated_at, { + id: "lastUpdated", + cell: (props) => , + header: "Updated", + }), + ], + [], + ); + + return columns; +}; + +export default useCatalogDatasetColumns; diff --git a/clients/admin-ui/src/features/data-catalog/projects/CatalogProjectsTable.tsx b/clients/admin-ui/src/features/data-catalog/projects/CatalogProjectsTable.tsx new file mode 100644 index 0000000000..a6e9ebb3cc --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/projects/CatalogProjectsTable.tsx @@ -0,0 +1,191 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { + ColumnDef, + createColumnHelper, + getCoreRowModel, + getExpandedRowModel, + getGroupedRowModel, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; +import { Text, VStack } from "fidesui"; +import { useRouter } from "next/router"; +import { useEffect, useMemo, useState } from "react"; + +import { DATA_CATALOG_ROUTE } from "~/features/common/nav/v2/routes"; +import { + DefaultCell, + FidesTableV2, + PaginationBar, + TableSkeletonLoader, + useServerSidePagination, +} from "~/features/common/table/v2"; +import { RelativeTimestampCell } from "~/features/common/table/v2/cells"; +import CatalogResourceNameCell from "~/features/data-catalog/CatalogResourceNameCell"; +import CatalogStatusBadgeCell from "~/features/data-catalog/CatalogStatusBadgeCell"; +import { useGetCatalogProjectsQuery } from "~/features/data-catalog/data-catalog.slice"; +import { getCatalogResourceStatus } from "~/features/data-catalog/utils"; +import { StagedResourceAPIResponse } from "~/types/api"; + +const EMPTY_RESPONSE = { + items: [], + total: 0, + page: 1, + size: 50, + pages: 1, +}; + +const EmptyTableNotice = () => ( + + + + No resources found + + + +); + +const columnHelper = createColumnHelper(); + +const CatalogProjectsTable = ({ + systemKey, + monitorConfigIds, +}: { + systemKey: string; + monitorConfigIds: string[]; +}) => { + const [rowSelectionState, setRowSelectionState] = useState( + {}, + ); + + const { + PAGE_SIZES, + pageSize, + setPageSize, + onPreviousPageClick, + isPreviousPageDisabled, + onNextPageClick, + isNextPageDisabled, + startRange, + endRange, + pageIndex, + setTotalPages, + } = useServerSidePagination(); + + const { + isFetching, + isLoading, + data: queryResult, + } = useGetCatalogProjectsQuery({ + page: pageIndex, + size: pageSize, + monitor_config_ids: monitorConfigIds, + }); + + const router = useRouter(); + + const { + items: data, + total: totalRows, + pages: totalPages, + } = useMemo(() => queryResult ?? EMPTY_RESPONSE, [queryResult]); + + useEffect(() => { + setTotalPages(totalPages); + }, [totalPages, setTotalPages]); + + const columns: ColumnDef[] = useMemo( + () => [ + columnHelper.accessor((row) => row.name, { + id: "name", + cell: (props) => ( + + ), + header: "Project", + }), + columnHelper.display({ + id: "status", + cell: ({ row }) => ( + + ), + header: "Status", + }), + columnHelper.accessor((row) => row.monitor_config_id, { + id: "monitorConfigId", + cell: (props) => , + header: "Detected by", + }), + columnHelper.accessor((row) => row.description, { + id: "description", + cell: (props) => , + header: "Description", + }), + columnHelper.accessor((row) => row.updated_at, { + id: "lastUpdated", + cell: (props) => , + header: "Updated", + meta: { + cellProps: { + borderRight: "none", + }, + }, + }), + ], + [], + ); + + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + getGroupedRowModel: getGroupedRowModel(), + getExpandedRowModel: getExpandedRowModel(), + manualPagination: true, + columnResizeMode: "onChange", + columns, + data, + getRowId: (row) => row.urn, + onRowSelectionChange: setRowSelectionState, + state: { + rowSelection: rowSelectionState, + }, + }); + + if (isLoading || isFetching) { + return ; + } + + return ( + <> + } + onRowClick={(row) => + router.push(`${DATA_CATALOG_ROUTE}/${systemKey}/projects/${row.urn}`) + } + /> + + + ); +}; + +export default CatalogProjectsTable; diff --git a/clients/admin-ui/src/features/data-catalog/staged-resources/CatalogResourcesTable.tsx b/clients/admin-ui/src/features/data-catalog/staged-resources/CatalogResourcesTable.tsx new file mode 100644 index 0000000000..bcb8b9600e --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/staged-resources/CatalogResourcesTable.tsx @@ -0,0 +1,157 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { + getCoreRowModel, + getExpandedRowModel, + getGroupedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Box, Flex } from "fidesui"; +import { useRouter } from "next/router"; +import { useEffect, useMemo, useState } from "react"; + +import { DATA_CATALOG_ROUTE } from "~/features/common/nav/v2/routes"; +import { + FidesTableV2, + PaginationBar, + TableActionBar, + TableSkeletonLoader, + useServerSidePagination, +} from "~/features/common/table/v2"; +import EmptyCatalogTableNotice from "~/features/data-catalog/datasets/EmptyCatalogTableNotice"; +import useCatalogResourceColumns from "~/features/data-catalog/useCatalogResourceColumns"; +import { useGetMonitorResultsQuery } from "~/features/data-discovery-and-detection/discovery-detection.slice"; +import { SearchInput } from "~/features/data-discovery-and-detection/SearchInput"; +import { StagedResourceType } from "~/features/data-discovery-and-detection/types/StagedResourceType"; +import { findResourceType } from "~/features/data-discovery-and-detection/utils/findResourceType"; +import resourceHasChildren from "~/features/data-discovery-and-detection/utils/resourceHasChildren"; +import { + DiffStatus, + StagedResourceAPIResponse, + SystemResponse, +} from "~/types/api"; + +// everything except muted +const DIFF_STATUS_FILTERS = [ + DiffStatus.ADDITION, + DiffStatus.CLASSIFYING, + DiffStatus.CLASSIFICATION_ADDITION, + DiffStatus.CLASSIFICATION_QUEUED, + DiffStatus.CLASSIFICATION_UPDATE, + DiffStatus.MONITORED, + DiffStatus.PROMOTING, + DiffStatus.REMOVAL, + DiffStatus.REMOVING, +]; + +const EMPTY_RESPONSE = { + items: [], + total: 0, + page: 1, + size: 50, + pages: 1, +}; + +const CatalogResourcesTable = ({ + resourceUrn, + system, +}: { + resourceUrn: string; + system: SystemResponse; +}) => { + const router = useRouter(); + + const { + PAGE_SIZES, + pageSize, + setPageSize, + onPreviousPageClick, + isPreviousPageDisabled, + onNextPageClick, + isNextPageDisabled, + startRange, + endRange, + pageIndex, + setTotalPages, + resetPageIndexToDefault, + } = useServerSidePagination(); + + const [searchQuery, setSearchQuery] = useState(""); + + useEffect(() => { + resetPageIndexToDefault(); + }, [resourceUrn, resetPageIndexToDefault]); + + const { + isFetching, + isLoading, + data: resources, + } = useGetMonitorResultsQuery({ + staged_resource_urn: resourceUrn, + page: pageIndex, + size: pageSize, + diff_status: DIFF_STATUS_FILTERS, + search: searchQuery, + }); + + const { + items: data, + total: totalRows, + pages: totalPages, + } = useMemo(() => resources ?? EMPTY_RESPONSE, [resources]); + + useEffect(() => { + setTotalPages(totalPages); + }, [totalPages, setTotalPages]); + + const type = findResourceType(data[0] ?? StagedResourceType.NONE); + + const columns = useCatalogResourceColumns(type); + + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + getGroupedRowModel: getGroupedRowModel(), + getExpandedRowModel: getExpandedRowModel(), + columns, + manualPagination: true, + data, + columnResizeMode: "onChange", + getRowId: (row) => row.urn, + }); + + if (isLoading || isFetching) { + return ; + } + + return ( + <> + + + + + + + + } + getRowIsClickable={(row) => resourceHasChildren(row)} + onRowClick={(row) => + router.push(`${DATA_CATALOG_ROUTE}/${system.fides_key}/${row.urn}`) + } + /> + + + ); +}; + +export default CatalogResourcesTable; diff --git a/clients/admin-ui/src/features/data-catalog/staged-resources/parseUrnToBreadcrumbs.ts b/clients/admin-ui/src/features/data-catalog/staged-resources/parseUrnToBreadcrumbs.ts new file mode 100644 index 0000000000..17974af525 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/staged-resources/parseUrnToBreadcrumbs.ts @@ -0,0 +1,23 @@ +import { NextBreadcrumbProps } from "~/features/common/nav/v2/NextBreadcrumb"; + +const parseUrnToBreadcrumbs = ( + urn: string, + urlPrefix: string, +): NextBreadcrumbProps["items"] => { + if (!urn) { + return []; + } + const urnParts = urn.split("."); + const breadcrumbItems: NextBreadcrumbProps["items"] = []; + urnParts.reduce((prev, current) => { + const next = `${prev}.${current}`; + breadcrumbItems.push({ + title: current, + href: `${urlPrefix}/${next}`, + }); + return next; + }); + return breadcrumbItems; +}; + +export default parseUrnToBreadcrumbs; diff --git a/clients/admin-ui/src/features/data-catalog/systems/CatalogSystemsTable.tsx b/clients/admin-ui/src/features/data-catalog/systems/CatalogSystemsTable.tsx new file mode 100644 index 0000000000..bfca1bd59c --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/systems/CatalogSystemsTable.tsx @@ -0,0 +1,186 @@ +/* eslint-disable react/no-unstable-nested-components */ + +import { + ColumnDef, + createColumnHelper, + getCoreRowModel, + getExpandedRowModel, + getGroupedRowModel, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; +import { useRouter } from "next/router"; +import { useEffect, useMemo, useState } from "react"; + +import { DATA_CATALOG_ROUTE } from "~/features/common/nav/v2/routes"; +import { + DefaultCell, + DefaultHeaderCell, + FidesTableV2, + PaginationBar, + TableSkeletonLoader, + useServerSidePagination, +} from "~/features/common/table/v2"; +import { getQueryParamsFromArray } from "~/features/common/utils"; +import { useGetCatalogSystemsQuery } from "~/features/data-catalog/data-catalog.slice"; +import EmptyCatalogTableNotice from "~/features/data-catalog/datasets/EmptyCatalogTableNotice"; +import EditDataUseCell from "~/features/data-catalog/systems/EditDataUseCell"; +import SystemActionsCell from "~/features/data-catalog/systems/SystemActionCell"; +import { useLazyGetAvailableDatabasesByConnectionQuery } from "~/features/data-discovery-and-detection/discovery-detection.slice"; +import { SystemWithMonitorKeys } from "~/types/api"; + +const EMPTY_RESPONSE = { + items: [], + total: 0, + page: 1, + size: 50, + pages: 1, +}; + +const columnHelper = createColumnHelper(); + +const SystemsTable = () => { + const [rowSelectionState, setRowSelectionState] = useState( + {}, + ); + + const router = useRouter(); + + const { + PAGE_SIZES, + pageSize, + setPageSize, + onPreviousPageClick, + isPreviousPageDisabled, + onNextPageClick, + isNextPageDisabled, + startRange, + endRange, + pageIndex, + setTotalPages, + } = useServerSidePagination(); + + const { data: queryResult, isLoading } = useGetCatalogSystemsQuery({ + page: pageIndex, + size: pageSize, + show_hidden: false, + }); + + const [getProjects] = useLazyGetAvailableDatabasesByConnectionQuery(); + + const { + items: data, + total: totalRows, + pages: totalPages, + } = useMemo(() => queryResult ?? EMPTY_RESPONSE, [queryResult]); + + useEffect(() => { + setTotalPages(totalPages); + }, [totalPages, setTotalPages]); + + const handleRowClicked = async (row: SystemWithMonitorKeys) => { + // if there are projects, go to project view; otherwise go to datasets view + const projectsResponse = await getProjects({ + connection_config_key: row.connection_configs!.key, + page: 1, + size: 1, + }); + + const hasProjects = !!projectsResponse?.data?.total; + const queryString = getQueryParamsFromArray( + row.monitor_config_keys ?? [], + "monitor_config_ids", + ); + + const url = `${DATA_CATALOG_ROUTE}/${row.fides_key}${hasProjects ? "/projects" : ""}?${queryString}`; + router.push(url); + }; + + const columns: ColumnDef[] = useMemo( + () => [ + columnHelper.accessor((row) => row.name, { + id: "name", + cell: ({ getValue, row }) => ( + + ), + header: (props) => , + }), + columnHelper.display({ + id: "data-uses", + cell: ({ row }) => , + header: (props) => , + meta: { + disableRowClick: true, + }, + minSize: 280, + }), + columnHelper.display({ + id: "actions", + cell: (props) => ( + + router.push(`/systems/configure/${props.row.original.fides_key}`) + } + /> + ), + maxSize: 20, + enableResizing: false, + meta: { + cellProps: { + borderLeft: "none", + }, + disableRowClick: true, + }, + }), + ], + [router], + ); + + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + getGroupedRowModel: getGroupedRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getRowId: (row) => row.fides_key, + manualPagination: true, + columnResizeMode: "onChange", + columns, + data, + onRowSelectionChange: setRowSelectionState, + state: { + rowSelection: rowSelectionState, + }, + }); + + if (isLoading) { + return ; + } + + return ( + <> + } + onRowClick={handleRowClicked} + getRowIsClickable={(row) => !!row.connection_configs?.key} + /> + + + ); +}; + +export default SystemsTable; diff --git a/clients/admin-ui/src/features/data-catalog/systems/EditDataUseCell.tsx b/clients/admin-ui/src/features/data-catalog/systems/EditDataUseCell.tsx new file mode 100644 index 0000000000..17d0a69527 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/systems/EditDataUseCell.tsx @@ -0,0 +1,125 @@ +import { + AntButton as Button, + Box, + CloseIcon, + EditIcon, + useDisclosure, +} from "fidesui"; +import { useState } from "react"; + +import DataUseSelect from "~/features/common/dropdown/DataUseSelect"; +import useTaxonomies from "~/features/common/hooks/useTaxonomies"; +import EditMinimalDataUseModal from "~/features/data-catalog/systems/EditMinimalDataUseModal"; +import TaxonomyAddButton from "~/features/data-discovery-and-detection/tables/cells/TaxonomyAddButton"; +import TaxonomyCellContainer from "~/features/data-discovery-and-detection/tables/cells/TaxonomyCellContainer"; +import TaxonomyBadge from "~/features/data-discovery-and-detection/TaxonomyBadge"; +import useSystemDataUseCrud from "~/features/data-use/useSystemDataUseCrud"; +import { + PrivacyDeclaration, + PrivacyDeclarationResponse, + SystemResponse, +} from "~/types/api"; + +interface EditDataUseCellProps { + system: SystemResponse; +} + +const DeleteDataUseButton = ({ onClick }: { onClick: () => void }) => ( + + + + + + )} + + ); +}; + +export default EditMinimalDataUseModal; diff --git a/clients/admin-ui/src/features/data-catalog/systems/SystemActionCell.tsx b/clients/admin-ui/src/features/data-catalog/systems/SystemActionCell.tsx new file mode 100644 index 0000000000..77e3ddec30 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/systems/SystemActionCell.tsx @@ -0,0 +1,39 @@ +import { + AntButton, + Menu, + MenuButton, + MenuItem, + MenuList, + MoreIcon, +} from "fidesui"; + +interface SystemActionsCellProps { + onDetailClick?: () => void; +} + +const SystemActionsCell = ({ onDetailClick }: SystemActionsCellProps) => { + return ( + + } + data-testid="system-actions-menu" + /> + + {onDetailClick && ( + + View details + + )} + + + ); +}; + +export default SystemActionsCell; diff --git a/clients/admin-ui/src/features/data-catalog/useCatalogResourceColumns.tsx b/clients/admin-ui/src/features/data-catalog/useCatalogResourceColumns.tsx new file mode 100644 index 0000000000..417485e0c4 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/useCatalogResourceColumns.tsx @@ -0,0 +1,128 @@ +import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; + +import { DefaultCell } from "~/features/common/table/v2"; +import { RelativeTimestampCell } from "~/features/common/table/v2/cells"; +import CatalogResourceActionsCell from "~/features/data-catalog/CatalogResourceActionsCell"; +import CatalogResourceNameCell from "~/features/data-catalog/CatalogResourceNameCell"; +import CatalogStatusBadgeCell from "~/features/data-catalog/CatalogStatusBadgeCell"; +import { getCatalogResourceStatus } from "~/features/data-catalog/utils"; +import EditCategoryCell from "~/features/data-discovery-and-detection/tables/cells/EditCategoryCell"; +import FieldDataTypeCell from "~/features/data-discovery-and-detection/tables/cells/FieldDataTypeCell"; +import { StagedResourceType } from "~/features/data-discovery-and-detection/types/StagedResourceType"; +import { StagedResourceAPIResponse } from "~/types/api"; + +const columnHelper = createColumnHelper(); + +const useCatalogResourceColumns = (type: StagedResourceType) => { + const defaultColumns: ColumnDef[] = []; + + if (!type) { + return defaultColumns; + } + + if (type === StagedResourceType.TABLE) { + const columnDefs = [ + columnHelper.display({ + id: "name", + cell: ({ row }) => , + header: "Table", + }), + columnHelper.display({ + id: "status", + cell: ({ row }) => ( + + ), + header: "Status", + }), + columnHelper.display({ + id: "category", + cell: ({ row }) => , + header: "Data categories", + minSize: 280, + meta: { + disableRowClick: true, + }, + }), + columnHelper.accessor((row) => row.description, { + id: "description", + cell: (props) => , + header: "Description", + }), + columnHelper.accessor((row) => row.updated_at, { + id: "lastUpdated", + cell: (props) => , + header: "Updated", + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => ( + + ), + header: "Actions", + meta: { + disableRowClick: true, + }, + }), + ]; + return columnDefs; + } + + if (type === StagedResourceType.FIELD) { + const columns = [ + columnHelper.display({ + id: "name", + cell: ({ row }) => , + header: "Field", + }), + columnHelper.display({ + id: "status", + cell: ({ row }) => ( + + ), + header: "Status", + }), + columnHelper.accessor((row) => row.data_type, { + id: "dataType", + cell: (props) => , + header: "Data type", + }), + columnHelper.display({ + id: "category", + cell: ({ row }) => , + header: "Data categories", + minSize: 280, + meta: { + disableRowClick: true, + }, + }), + columnHelper.accessor((row) => row.description, { + id: "description", + cell: (props) => , + header: "Description", + }), + columnHelper.accessor((row) => row.updated_at, { + id: "lastUpdated", + cell: (props) => , + header: "Updated", + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => ( + + ), + header: "Actions", + meta: { + disableRowClick: true, + }, + }), + ]; + return columns; + } + return defaultColumns; +}; + +export default useCatalogResourceColumns; diff --git a/clients/admin-ui/src/features/data-catalog/utils.ts b/clients/admin-ui/src/features/data-catalog/utils.ts new file mode 100644 index 0000000000..f1b2b3fcf5 --- /dev/null +++ b/clients/admin-ui/src/features/data-catalog/utils.ts @@ -0,0 +1,50 @@ +import { DiffStatus, StagedResourceAPIResponse } from "~/types/api"; + +export enum CatalogResourceStatus { + ATTENTION_REQUIRED = "Attention required", + IN_REVIEW = "In review", + APPROVED = "Approved", + CLASSIFYING = "Classifying", +} + +export const getCatalogResourceStatus = ( + resource: StagedResourceAPIResponse, +) => { + const resourceSchemaChanged = + resource.diff_status === DiffStatus.ADDITION || + resource.diff_status === DiffStatus.REMOVAL; + const resourceChildrenSchemaChanged = + resource.child_diff_statuses && + (resource.child_diff_statuses[DiffStatus.ADDITION] || + resource.child_diff_statuses[DiffStatus.REMOVAL]); + + if (resourceSchemaChanged || resourceChildrenSchemaChanged) { + return CatalogResourceStatus.ATTENTION_REQUIRED; + } + + const classificationInProgress = + resource.diff_status === DiffStatus.CLASSIFICATION_QUEUED || + resource.diff_status === DiffStatus.CLASSIFYING; + const childClassificationInProgress = + resource.child_diff_statuses && + (resource.child_diff_statuses[DiffStatus.CLASSIFICATION_QUEUED] || + resource.child_diff_statuses[DiffStatus.CLASSIFYING]); + + if (classificationInProgress || childClassificationInProgress) { + return CatalogResourceStatus.CLASSIFYING; + } + + const classificationChanged = + resource.diff_status === DiffStatus.CLASSIFICATION_ADDITION || + resource.diff_status === DiffStatus.CLASSIFICATION_UPDATE; + const childClassificationChanged = + resource.child_diff_statuses && + (resource.child_diff_statuses[DiffStatus.CLASSIFICATION_ADDITION] || + resource.child_diff_statuses[DiffStatus.CLASSIFICATION_UPDATE]); + + if (classificationChanged || childClassificationChanged) { + return CatalogResourceStatus.IN_REVIEW; + } + + return CatalogResourceStatus.APPROVED; +}; diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/TaxonomyBadge.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/TaxonomyBadge.tsx new file mode 100644 index 0000000000..067ea7893a --- /dev/null +++ b/clients/admin-ui/src/features/data-discovery-and-detection/TaxonomyBadge.tsx @@ -0,0 +1,40 @@ +import { Flex, FlexProps } from "fidesui"; +import React from "react"; + +interface TaxonomyBadgeProps extends FlexProps { + children: React.ReactNode; + closeButton?: React.ReactNode; +} + +const TaxonomyBadge = ({ + children, + onClick, + closeButton, + ...props +}: TaxonomyBadgeProps) => { + return ( + + + {children} + + {closeButton} + + ); +}; + +export default TaxonomyBadge; diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/discovery-detection.slice.ts b/clients/admin-ui/src/features/data-discovery-and-detection/discovery-detection.slice.ts index e12c578cbc..eaf100979f 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/discovery-detection.slice.ts +++ b/clients/admin-ui/src/features/data-discovery-and-detection/discovery-detection.slice.ts @@ -34,6 +34,7 @@ interface DatabaseByMonitorQueryParams { page: number; size: number; monitor_config_id: string; + show_hidden?: boolean; } interface DatabaseByConnectionQueryParams { @@ -90,7 +91,7 @@ const discoveryDetectionApi = baseApi.injectEndpoints({ }), }, ), - getDatabasesByConnection: build.query< + getAvailableDatabasesByConnection: build.query< Page_str_, DatabaseByConnectionQueryParams >({ @@ -230,8 +231,8 @@ export const { useGetMonitorsByIntegrationQuery, usePutDiscoveryMonitorMutation, useGetDatabasesByMonitorQuery, - useGetDatabasesByConnectionQuery, - useLazyGetDatabasesByConnectionQuery, + useGetAvailableDatabasesByConnectionQuery, + useLazyGetAvailableDatabasesByConnectionQuery, useExecuteDiscoveryMonitorMutation, useDeleteDiscoveryMonitorMutation, useGetMonitorResultsQuery, diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDetectionResultColumns.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDetectionResultColumns.tsx index e01e585562..32a451bba9 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDetectionResultColumns.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDetectionResultColumns.tsx @@ -4,8 +4,8 @@ import { DefaultCell, DefaultHeaderCell } from "~/features/common/table/v2"; import { RelativeTimestampCell } from "~/features/common/table/v2/cells"; import DetectionItemActionsCell from "~/features/data-discovery-and-detection/tables/cells/DetectionItemActionsCell"; import FieldDataTypeCell from "~/features/data-discovery-and-detection/tables/cells/FieldDataTypeCell"; -import ResultStatusBadgeCell from "~/features/data-discovery-and-detection/tables/cells/ResultStatusBadgeCell"; import ResultStatusCell from "~/features/data-discovery-and-detection/tables/cells/ResultStatusCell"; +import ResultStatusBadgeCell from "~/features/data-discovery-and-detection/tables/cells/StagedResourceStatusBadgeCell"; import { DiscoveryMonitorItem } from "~/features/data-discovery-and-detection/types/DiscoveryMonitorItem"; import { ResourceChangeType } from "~/features/data-discovery-and-detection/types/ResourceChangeType"; import { StagedResourceType } from "~/features/data-discovery-and-detection/types/StagedResourceType"; diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDiscoveryResultColumns.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDiscoveryResultColumns.tsx index c37b67bc72..27ff6cbe1f 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDiscoveryResultColumns.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/hooks/useDiscoveryResultColumns.tsx @@ -6,7 +6,7 @@ import { RelativeTimestampCell, } from "~/features/common/table/v2/cells"; import FieldDataTypeCell from "~/features/data-discovery-and-detection/tables/cells/FieldDataTypeCell"; -import ResultStatusBadgeCell from "~/features/data-discovery-and-detection/tables/cells/ResultStatusBadgeCell"; +import ResultStatusBadgeCell from "~/features/data-discovery-and-detection/tables/cells/StagedResourceStatusBadgeCell"; import { DiscoveryMonitorItem } from "~/features/data-discovery-and-detection/types/DiscoveryMonitorItem"; import { ResourceChangeType } from "~/features/data-discovery-and-detection/types/ResourceChangeType"; import { StagedResourceType } from "~/features/data-discovery-and-detection/types/StagedResourceType"; @@ -14,7 +14,7 @@ import findProjectFromUrn from "~/features/data-discovery-and-detection/utils/fi import { DiffStatus } from "~/types/api"; import DiscoveryItemActionsCell from "../tables/cells/DiscoveryItemActionsCell"; -import EditCategoriesCell from "../tables/cells/EditCategoryCell"; +import EditCategoryCell from "../tables/cells/EditCategoryCell"; import ResultStatusCell from "../tables/cells/ResultStatusCell"; const useDiscoveryResultColumns = ({ @@ -182,7 +182,7 @@ const useDiscoveryResultColumns = ({ columnHelper.display({ id: "classifications", cell: ({ row }) => { - return ; + return ; }, meta: { overflow: "visible", disableRowClick: true }, header: "Data category", diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/tables/ActivityTable.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/tables/ActivityTable.tsx index 0828ee9d2e..d661b58936 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/tables/ActivityTable.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/tables/ActivityTable.tsx @@ -23,8 +23,8 @@ import { import { RelativeTimestampCell } from "~/features/common/table/v2/cells"; import { useGetMonitorResultsQuery } from "~/features/data-discovery-and-detection/discovery-detection.slice"; import IconLegendTooltip from "~/features/data-discovery-and-detection/IndicatorLegend"; -import ResultStatusBadgeCell from "~/features/data-discovery-and-detection/tables/cells/ResultStatusBadgeCell"; import ResultStatusCell from "~/features/data-discovery-and-detection/tables/cells/ResultStatusCell"; +import ResultStatusBadgeCell from "~/features/data-discovery-and-detection/tables/cells/StagedResourceStatusBadgeCell"; import getResourceRowName from "~/features/data-discovery-and-detection/utils/getResourceRowName"; import { DiffStatus, diff --git a/clients/admin-ui/src/features/data-discovery-and-detection/tables/cells/EditCategoryCell.tsx b/clients/admin-ui/src/features/data-discovery-and-detection/tables/cells/EditCategoryCell.tsx index 05310a081f..45d08ff0a4 100644 --- a/clients/admin-ui/src/features/data-discovery-and-detection/tables/cells/EditCategoryCell.tsx +++ b/clients/admin-ui/src/features/data-discovery-and-detection/tables/cells/EditCategoryCell.tsx @@ -1,39 +1,32 @@ -import { - AntButton as Button, - AntButtonProps as ButtonProps, - Box, - CloseIcon, - EditIcon, - SmallAddIcon, - Text, - Wrap, -} from "fidesui"; +import { AntButton as Button, Box, CloseIcon, EditIcon } from "fidesui"; import { useState } from "react"; -import { TaxonomySelect } from "~/features/common/dropdown/TaxonomySelect"; +import DataCategorySelect from "~/features/common/dropdown/DataCategorySelect"; import useTaxonomies from "~/features/common/hooks/useTaxonomies"; import { SparkleIcon } from "~/features/common/Icon/SparkleIcon"; -import ClassificationCategoryBadge from "~/features/data-discovery-and-detection/ClassificationCategoryBadge"; +import TaxonomyAddButton from "~/features/data-discovery-and-detection/tables/cells/TaxonomyAddButton"; +import TaxonomyCellContainer from "~/features/data-discovery-and-detection/tables/cells/TaxonomyCellContainer"; +import TaxonomyBadge from "~/features/data-discovery-and-detection/TaxonomyBadge"; import { DiscoveryMonitorItem } from "~/features/data-discovery-and-detection/types/DiscoveryMonitorItem"; import { useUpdateResourceCategoryMutation } from "../../discovery-detection.slice"; -const AddCategoryButton = (props: ButtonProps) => ( +interface EditCategoryCellProps { + resource: DiscoveryMonitorItem; +} + +const DeleteCategoryButton = ({ onClick }: { onClick: () => void }) => (