From a8ef950b82bb52c53e3fef5c2d68a9dafe908f01 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Fri, 16 Aug 2024 09:52:07 -0300 Subject: [PATCH 01/12] PROD-2606 WIP --- clients/admin-ui/src/pages/dataset/[id]/[urn].tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx b/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx index 9c5d38cd8f..6238c23d58 100644 --- a/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx +++ b/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx @@ -115,6 +115,15 @@ const FieldsDetailPage: NextPage = () => { [collection, collections, dataset, updateDataset], ); + const handleRowClick = useCallback( + (row: DatasetField) => { + router.push({ + pathname: `/dataset/${datasetId}/${urn}/fields/${row.name}`, + }); + }, + [datasetId, router, urn], + ); + const columns = useMemo( () => [ columnHelper.accessor((row) => row.name, { @@ -255,6 +264,7 @@ const FieldsDetailPage: NextPage = () => { } + onRowClick={handleRowClick} /> Date: Mon, 19 Aug 2024 09:04:47 -0300 Subject: [PATCH 02/12] PROD-2606 WIP --- .../src/features/common/table/v2/cells.tsx | 4 ++- .../admin-ui/src/pages/dataset/[id]/[urn].tsx | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/clients/admin-ui/src/features/common/table/v2/cells.tsx b/clients/admin-ui/src/features/common/table/v2/cells.tsx index 9e03ffd75e..f5925dd498 100644 --- a/clients/admin-ui/src/features/common/table/v2/cells.tsx +++ b/clients/admin-ui/src/features/common/table/v2/cells.tsx @@ -26,9 +26,10 @@ import { RTKResult } from "~/types/errors"; export const DefaultCell = ({ value, + ...chakraStyleProps }: { value: string | undefined | number | boolean; -}) => ( +} & TextProps) => ( {value !== null && value !== undefined ? value.toString() : value} diff --git a/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx b/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx index 6238c23d58..5f9c5b2e05 100644 --- a/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx +++ b/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx @@ -117,9 +117,13 @@ const FieldsDetailPage: NextPage = () => { const handleRowClick = useCallback( (row: DatasetField) => { - router.push({ - pathname: `/dataset/${datasetId}/${urn}/fields/${row.name}`, - }); + const hasSubfields = row.fields && row.fields?.length > 0; + + if (hasSubfields) { + router.push({ + pathname: `/dataset/${datasetId}/${urn}/fields/${row.name}`, + }); + } }, [datasetId, router, urn], ); @@ -128,7 +132,16 @@ const FieldsDetailPage: NextPage = () => { () => [ columnHelper.accessor((row) => row.name, { id: "name", - cell: (props) => , + cell: (props) => { + const hasSubfields = + props.row.original.fields && props.row.original.fields?.length > 0; + return ( + + ); + }, header: (props) => , size: 180, }), @@ -265,6 +278,12 @@ const FieldsDetailPage: NextPage = () => { tableInstance={tableInstance} emptyTableNotice={} onRowClick={handleRowClick} + getRowIsClickable={(row) => { + const hasSubfields = Boolean( + row.fields && row.fields?.length > 0, + ); + return hasSubfields; + }} /> Date: Mon, 19 Aug 2024 09:19:45 -0300 Subject: [PATCH 03/12] PROD-2606 Reorganize / rename page files for clarity --- .../admin-ui/src/features/common/nav/v2/routes.ts | 9 +++++++-- .../[urn].tsx => [datasetId]/[collectionName].tsx} | 12 +++++------- .../pages/dataset/{[id] => [datasetId]}/index.tsx | 11 +++++------ clients/admin-ui/src/pages/dataset/index.tsx | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) rename clients/admin-ui/src/pages/dataset/{[id]/[urn].tsx => [datasetId]/[collectionName].tsx} (96%) rename clients/admin-ui/src/pages/dataset/{[id] => [datasetId]}/index.tsx (96%) 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 ef6fd9980c..204e91140c 100644 --- a/clients/admin-ui/src/features/common/nav/v2/routes.ts +++ b/clients/admin-ui/src/features/common/nav/v2/routes.ts @@ -10,9 +10,14 @@ export const REPORTING_DATAMAP_ROUTE = "/reporting/datamap"; export const SYSTEM_ROUTE = "/systems"; export const EDIT_SYSTEM_ROUTE = "/systems/configure/[id]"; export const CLASSIFY_SYSTEMS_ROUTE = "/classify-systems"; + +// Dataset export const DATASET_ROUTE = "/dataset"; -export const DATASET_DETAIL_ROUTE = "/dataset/[id]"; -export const DATASET_URL_DETAIL_ROUTE = "/dataset/[id]/[urn]"; +export const DATASET_DETAIL_ROUTE = "/dataset/[datasetId]"; +export const DATASET_COLLECTION_DETAIL_ROUTE = + "/dataset/[datasetId]/[collectionName]"; +export const DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE = + "/dataset/[datasetId]/[collectionName]/[subfieldName]"; // Detection and discovery export const DETECTION_DISCOVERY_ACTIVITY_ROUTE = "/data-discovery/activity"; diff --git a/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx b/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName].tsx similarity index 96% rename from clients/admin-ui/src/pages/dataset/[id]/[urn].tsx rename to clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName].tsx index 5f9c5b2e05..561bddcede 100644 --- a/clients/admin-ui/src/pages/dataset/[id]/[urn].tsx +++ b/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName].tsx @@ -46,10 +46,8 @@ const FieldsDetailPage: NextPage = () => { const router = useRouter(); const [updateDataset] = useUpdateDatasetMutation(); - const { id: idParam, urn: urnParam } = router.query; - const datasetId = Array.isArray(idParam) ? idParam[0] : idParam!; - const urn = Array.isArray(urnParam) ? urnParam[0] : urnParam; - const collectionName = urn?.split(".")[0] || ""; + const datasetId = router.query.datasetId as string; + const collectionName = router.query.collectionName as string; const { isLoading, data: dataset } = useGetDatasetByKeyQuery(datasetId); const collections = useMemo(() => dataset?.collections || [], [dataset]); @@ -121,11 +119,11 @@ const FieldsDetailPage: NextPage = () => { if (hasSubfields) { router.push({ - pathname: `/dataset/${datasetId}/${urn}/fields/${row.name}`, + pathname: `/dataset/${datasetId}/${collectionName}/fields/${row.name}`, }); } }, - [datasetId, router, urn], + [datasetId, router, collectionName], ); const columns = useMemo( @@ -250,7 +248,7 @@ const FieldsDetailPage: NextPage = () => { title: datasetId, link: { pathname: DATASET_DETAIL_ROUTE, - query: { id: datasetId }, + query: { datasetId }, }, icon: , }, diff --git a/clients/admin-ui/src/pages/dataset/[id]/index.tsx b/clients/admin-ui/src/pages/dataset/[datasetId]/index.tsx similarity index 96% rename from clients/admin-ui/src/pages/dataset/[id]/index.tsx rename to clients/admin-ui/src/pages/dataset/[datasetId]/index.tsx index 7b85e40da4..54cac4fee6 100644 --- a/clients/admin-ui/src/pages/dataset/[id]/index.tsx +++ b/clients/admin-ui/src/pages/dataset/[datasetId]/index.tsx @@ -16,8 +16,8 @@ import { DatabaseIcon } from "~/features/common/Icon/database/DatabaseIcon"; import { DatasetIcon } from "~/features/common/Icon/database/DatasetIcon"; import Layout from "~/features/common/Layout"; import { + DATASET_COLLECTION_DETAIL_ROUTE, DATASET_ROUTE, - DATASET_URL_DETAIL_ROUTE, } from "~/features/common/nav/v2/routes"; import PageHeader from "~/features/common/PageHeader"; import { @@ -37,8 +37,7 @@ const columnHelper = createColumnHelper(); const DatasetDetailPage: NextPage = () => { const router = useRouter(); - const { id } = router.query; - const datasetId = Array.isArray(id) ? id[0] : id!; + const datasetId = router.query.datasetId as string; const { isLoading, data: dataset } = useGetDatasetByKeyQuery(datasetId); const collections = useMemo(() => dataset?.collections || [], [dataset]); @@ -115,10 +114,10 @@ const DatasetDetailPage: NextPage = () => { const handleRowClick = (collection: DatasetCollection) => { router.push({ - pathname: DATASET_URL_DETAIL_ROUTE, + pathname: DATASET_COLLECTION_DETAIL_ROUTE, query: { - id: datasetId, - urn: collection.name, + datasetId, + collectionName: collection.name, }, }); }; diff --git a/clients/admin-ui/src/pages/dataset/index.tsx b/clients/admin-ui/src/pages/dataset/index.tsx index 7affb91ebc..cbd705a146 100644 --- a/clients/admin-ui/src/pages/dataset/index.tsx +++ b/clients/admin-ui/src/pages/dataset/index.tsx @@ -106,7 +106,7 @@ const DataSets: NextPage = () => { router.push({ pathname: DATASET_DETAIL_ROUTE, query: { - id: dataset.fides_key, + datasetId: dataset.fides_key, }, }); }, From 1cb8abd26d7f1846b74ed0ab2a0e1d7bb5784ca7 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Mon, 19 Aug 2024 10:26:45 -0300 Subject: [PATCH 04/12] PROD-2606 Implement subfields page and navigation --- .../src/features/common/nav/v2/routes.ts | 2 +- .../[collectionName]/[subfieldUrn].tsx | 353 ++++++++++++++++++ .../index.tsx} | 16 +- 3 files changed, 363 insertions(+), 8 deletions(-) create mode 100644 clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/[subfieldUrn].tsx rename clients/admin-ui/src/pages/dataset/[datasetId]/{[collectionName].tsx => [collectionName]/index.tsx} (97%) 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 204e91140c..514e6f0026 100644 --- a/clients/admin-ui/src/features/common/nav/v2/routes.ts +++ b/clients/admin-ui/src/features/common/nav/v2/routes.ts @@ -17,7 +17,7 @@ export const DATASET_DETAIL_ROUTE = "/dataset/[datasetId]"; export const DATASET_COLLECTION_DETAIL_ROUTE = "/dataset/[datasetId]/[collectionName]"; export const DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE = - "/dataset/[datasetId]/[collectionName]/[subfieldName]"; + "/dataset/[datasetId]/[collectionName]/[subfieldUrn]"; // Detection and discovery export const DETECTION_DISCOVERY_ACTIVITY_ROUTE = "/data-discovery/activity"; diff --git a/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/[subfieldUrn].tsx b/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/[subfieldUrn].tsx new file mode 100644 index 0000000000..b1f160b62c --- /dev/null +++ b/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/[subfieldUrn].tsx @@ -0,0 +1,353 @@ +/* eslint-disable react/no-unstable-nested-components */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { + createColumnHelper, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Box, Button, EditIcon, HStack, Text, VStack } from "fidesui"; +import type { NextPage } from "next"; +import { useRouter } from "next/router"; +import { useCallback, useMemo, useState } from "react"; + +import { DatabaseIcon } from "~/features/common/Icon/database/DatabaseIcon"; +import { DatasetIcon } from "~/features/common/Icon/database/DatasetIcon"; +import { FieldIcon } from "~/features/common/Icon/database/FieldIcon"; +import { TableIcon } from "~/features/common/Icon/database/TableIcon"; +import Layout from "~/features/common/Layout"; +import { + DATASET_COLLECTION_DETAIL_ROUTE, + DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE, + DATASET_DETAIL_ROUTE, + DATASET_ROUTE, +} from "~/features/common/nav/v2/routes"; +import PageHeader from "~/features/common/PageHeader"; +import { + BadgeCell, + DefaultCell, + DefaultHeaderCell, + FidesTableV2, + GlobalFilterV2, + TableActionBar, + TableSkeletonLoader, +} from "~/features/common/table/v2"; +import TaxonomiesPicker from "~/features/common/TaxonomiesPicker"; +import { + useGetDatasetByKeyQuery, + useUpdateDatasetMutation, +} from "~/features/dataset"; +import DatasetBreadcrumbs from "~/features/dataset/DatasetBreadcrumbs"; +import EditFieldDrawer from "~/features/dataset/EditFieldDrawer"; +import { getUpdatedDatasetFromField } from "~/features/dataset/helpers"; +import { DatasetField } from "~/types/api"; + +const columnHelper = createColumnHelper(); + +const FieldsDetailPage: NextPage = () => { + const router = useRouter(); + const [updateDataset] = useUpdateDatasetMutation(); + + const datasetId = router.query.datasetId as string; + const collectionName = router.query.collectionName as string; + const subfieldUrn = router.query.subfieldUrn as string; + const subfieldParts = subfieldUrn.split("."); + + const { isLoading, data: dataset } = useGetDatasetByKeyQuery(datasetId); + const collections = useMemo(() => dataset?.collections || [], [dataset]); + const collection = collections.find((c) => c.name === collectionName); + + const fields: DatasetField[] = useMemo( + () => collection?.fields || [], + [collection], + ); + + const subfields: DatasetField[] = useMemo(() => { + let currentSubfields = fields; + subfieldParts.forEach((subfield) => { + const field = currentSubfields.find((f) => f.name === subfield); + currentSubfields = field?.fields || []; + }); + return currentSubfields; + }, [fields, subfieldParts]); + + const [globalFilter, setGlobalFilter] = useState(); + + const handleAddDataCategory = useCallback( + ({ + dataCategory, + field, + }: { + dataCategory: string; + field: DatasetField; + }) => { + const dataCategories = field.data_categories || []; + const updatedField = { + ...field!, + data_categories: [...dataCategories, dataCategory], + }; + const collectionIndex = collections.indexOf(collection!); + const fieldIndex = collection!.fields.indexOf(field!); + const updatedDataset = getUpdatedDatasetFromField( + dataset!, + updatedField, + collectionIndex, + fieldIndex, + ); + updateDataset(updatedDataset); + }, + [collection, collections, dataset, updateDataset], + ); + + const handleRemoveDataCategory = useCallback( + ({ + dataCategory, + field, + }: { + dataCategory: string; + field: DatasetField; + }) => { + const updatedField = { + ...field!, + data_categories: field!.data_categories?.filter( + (dc) => dc !== dataCategory, + ), + }; + const collectionIndex = collections.indexOf(collection!); + const fieldIndex = collection!.fields.indexOf(field!); + const updatedDataset = getUpdatedDatasetFromField( + dataset!, + updatedField, + collectionIndex, + fieldIndex, + ); + updateDataset(updatedDataset); + }, + [collection, collections, dataset, updateDataset], + ); + + const handleRowClick = useCallback( + (row: DatasetField) => { + router.push({ + pathname: DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE, + query: { + datasetId, + collectionName, + subfieldUrn: `${subfieldUrn}.${row.name}`, + }, + }); + }, + [datasetId, router, collectionName, subfieldUrn], + ); + + const columns = useMemo( + () => [ + columnHelper.accessor((row) => row.name, { + id: "name", + cell: (props) => { + const hasSubfields = + props.row.original.fields && props.row.original.fields?.length > 0; + return ( + + ); + }, + header: (props) => , + size: 180, + }), + columnHelper.accessor((row) => row.fides_meta?.data_type, { + id: "type", + cell: (props) => + props.getValue() ? ( + + ) : ( + + ), + header: (props) => , + size: 80, + }), + columnHelper.accessor((row) => row.description, { + id: "description", + cell: (props) => , + header: (props) => , + size: 300, + }), + columnHelper.accessor((row) => row.data_categories, { + id: "data_categories", + cell: (props) => { + const field = props.row.original; + return ( + + handleAddDataCategory({ dataCategory, field }) + } + onRemoveTaxonomy={(dataCategory) => + handleRemoveDataCategory({ dataCategory, field }) + } + /> + ); + }, + header: (props) => ( + + ), + size: 300, + }), + + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => { + const field = row.original; + return ( + + + + ); + }, + meta: { + disableRowClick: true, + }, + }), + ], + [handleAddDataCategory, handleRemoveDataCategory], + ); + + const filteredSubfields = useMemo(() => { + if (!globalFilter) { + return subfields; + } + + return subfields.filter((f) => + f.name.toLowerCase().includes(globalFilter.toLowerCase()), + ); + }, [subfields, globalFilter]); + + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + columns, + data: filteredSubfields, + }); + + const [isEditingField, setIsEditingField] = useState(false); + const [selectedFieldForEditing, setSelectedFieldForEditing] = useState< + DatasetField | undefined + >(); + + return ( + + + , + link: DATASET_ROUTE, + }, + { + title: datasetId, + link: { + pathname: DATASET_DETAIL_ROUTE, + query: { datasetId }, + }, + icon: , + }, + { + title: collectionName, + icon: , + link: { + pathname: DATASET_COLLECTION_DETAIL_ROUTE, + query: { datasetId, collectionName }, + }, + }, + ...subfieldParts.map((subFieldName, index) => ({ + title: subFieldName, + link: { + pathname: DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE, + query: { + datasetId, + collectionName, + subfieldUrn: subfieldParts.slice(0, index + 1).join("."), + }, + }, + icon: , + })), + ]} + /> + + + {isLoading ? ( + + ) : ( + + + + + } + onRowClick={handleRowClick} + getRowIsClickable={(row) => { + const hasSubfields = Boolean( + row.fields && row.fields?.length > 0, + ); + return hasSubfields; + }} + /> + { + setIsEditingField(false); + setSelectedFieldForEditing(undefined); + }} + field={selectedFieldForEditing} + dataset={dataset!} + collection={collection!} + /> + + )} + + ); +}; + +const EmptyTableNotice = () => ( + + + + No fields found. + + + +); + +export default FieldsDetailPage; diff --git a/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName].tsx b/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/index.tsx similarity index 97% rename from clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName].tsx rename to clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/index.tsx index 561bddcede..509cc25474 100644 --- a/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName].tsx +++ b/clients/admin-ui/src/pages/dataset/[datasetId]/[collectionName]/index.tsx @@ -17,6 +17,7 @@ import { DatasetIcon } from "~/features/common/Icon/database/DatasetIcon"; import { TableIcon } from "~/features/common/Icon/database/TableIcon"; import Layout from "~/features/common/Layout"; import { + DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE, DATASET_DETAIL_ROUTE, DATASET_ROUTE, } from "~/features/common/nav/v2/routes"; @@ -115,13 +116,14 @@ const FieldsDetailPage: NextPage = () => { const handleRowClick = useCallback( (row: DatasetField) => { - const hasSubfields = row.fields && row.fields?.length > 0; - - if (hasSubfields) { - router.push({ - pathname: `/dataset/${datasetId}/${collectionName}/fields/${row.name}`, - }); - } + router.push({ + pathname: DATASET_COLLECTION_SUBFIELD_DETAIL_ROUTE, + query: { + datasetId, + collectionName, + subfieldUrn: row.name, + }, + }); }, [datasetId, router, collectionName], ); From 37d40b065a66b042bf7245fe2bece3fae00578cc Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Mon, 19 Aug 2024 10:31:38 -0300 Subject: [PATCH 05/12] PROD-2606 Move delete button location in dataset edit drawers --- .../src/features/common/EditDrawer.tsx | 35 ++++++++----------- .../features/dataset/EditCollectionDrawer.tsx | 9 +++-- .../features/dataset/EditDatasetDrawer.tsx | 9 ++--- .../src/features/dataset/EditFieldDrawer.tsx | 9 ++--- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/clients/admin-ui/src/features/common/EditDrawer.tsx b/clients/admin-ui/src/features/common/EditDrawer.tsx index 3a449ec935..0a64400372 100644 --- a/clients/admin-ui/src/features/common/EditDrawer.tsx +++ b/clients/admin-ui/src/features/common/EditDrawer.tsx @@ -24,32 +24,16 @@ interface Props { footer?: ReactNode; } -export const EditDrawerHeader = ({ - title, - onDelete, -}: { - title: string; - onDelete?: () => void; -}) => ( +export const EditDrawerHeader = ({ title }: { title: string }) => ( {title} - {onDelete ? ( - } - size="sm" - onClick={onDelete} - data-testid="delete-btn" - /> - ) : null} ); export const EditDrawerFooter = ({ - onClose, + onDelete, formId, isSaving, }: { @@ -59,11 +43,20 @@ export const EditDrawerFooter = ({ */ formId?: string; isSaving?: boolean; + onDelete?: () => void; } & Pick) => ( - + {onDelete ? ( + } + size="sm" + onClick={onDelete} + data-testid="delete-btn" + /> + ) : null} +