Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PROD-2606 Add-support-for-nested-fields-in-Dataset-Field-Detail-page #5216

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ The types of changes are:
- Fix white screen issue when privacy request has null value for daysLeft [#5213](https://github.com/ethyca/fides/pull/5213)


### Added
- Added support for navigating and editing nested fields in the Datasets page [#5216](https://github.com/ethyca/fides/pull/5216)


## [2.43.0](https://github.com/ethyca/fides/compare/2.42.1...2.43.0)

### Added
Expand Down
115 changes: 0 additions & 115 deletions clients/admin-ui/__tests__/features/dataset-helpers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import {
getUpdatedCollectionFromField,
getUpdatedDatasetFromClassifyDataset,
getUpdatedDatasetFromCollection,
getUpdatedDatasetFromField,
removeCollectionFromDataset,
removeFieldFromDataset,
} from "~/features/dataset/helpers";
import {
mockClassification,
mockClassifyCollection,
mockClassifyDataset,
mockClassifyField,
mockDataset,
mockDatasetCollection,
mockDatasetField,
Expand Down Expand Up @@ -76,118 +70,9 @@ describe("dataset helpers", () => {
updatedDataset.collections[collectionIndex].fields[fieldIndex].name,
).toEqual(newName);
});

it("should update a Dataset from a ClassifyInstance", () => {
const originalDataset = mockDataset({
collections: [
mockDatasetCollection({
name: "users",
fields: [
// Field without data categories:
mockDatasetField({
name: "email",
data_categories: [],
}),
// Field with data categories:
mockDatasetField({
name: "state",
data_categories: ["system.operations"],
}),
// Field without a corresponding classify field:
mockDatasetField({
name: "shoe_size",
data_categories: [],
}),
],
}),
],
});
const classifyDataset = mockClassifyDataset({
collections: [
mockClassifyCollection({
name: "users",
fields: [
mockClassifyField({
name: "email",
classifications: [
mockClassification({
label: "user.contact",
aggregated_score: 0.75,
}),
mockClassification({
label: "user.email",
aggregated_score: 0.95,
}),
],
}),
mockClassifyField({
name: "state",
classifications: [
mockClassification({
label: "user.address",
}),
],
}),
],
}),
],
});

const updatedDataset = getUpdatedDatasetFromClassifyDataset(
originalDataset,
classifyDataset,
classifyDataset.collections[0].name,
);

// It should return a new object.
expect(updatedDataset).not.toBe(originalDataset);
// A field without any categories should be filled in with the high score suggestion.
expect(updatedDataset.collections[0].fields[0].data_categories).toEqual([
"user.email",
]);
// A field that already has a category should be unchanged.
expect(updatedDataset.collections[0].fields[1].data_categories).toEqual([
"system.operations",
]);
// A field that had no classification match should be unchanged.
expect(updatedDataset.collections[0].fields[2].data_categories).toEqual(
[],
);
});
});

describe("removing from datasets", () => {
it("should be able to remove a dataset field", () => {
const deleteName = "remove me";
const fieldToBeRemoved = mockDatasetField({ name: deleteName });
const collection = mockDatasetCollection({
fields: [mockDatasetField(), fieldToBeRemoved, mockDatasetField()],
});
const dataset = mockDataset({
collections: [
mockDatasetCollection(),
mockDatasetCollection(),
collection,
],
});
const fieldIndex = 1;
const collectionIndex = 2;
const updatedDataset = removeFieldFromDataset(
dataset,
collectionIndex,
fieldIndex,
);

expect(updatedDataset.collections[collectionIndex].fields).toHaveLength(
2,
);
expect(
updatedDataset.collections[collectionIndex].fields.filter(
(f) => f.name === deleteName,
),
).toHaveLength(0);
});

it("should be able to remove a dataset collection", () => {
const deleteName = "remove me";
const collection = mockDatasetCollection({ name: deleteName });
Expand Down
46 changes: 46 additions & 0 deletions clients/admin-ui/cypress/e2e/datasets.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,52 @@ describe("Dataset", () => {
cy.getByTestId("row-0-col-name").contains("uuid");
cy.getByTestId("row-1-col-name").should("not.exist");
});

it("Can navigate to a subfields view", () => {
cy.visit("/dataset/demo_users_dataset/users");
cy.getByTestId("row-6").contains("workplace_info").click();
cy.url().should(
"contain",
"/dataset/demo_users_dataset/users/workplace_info",
);
cy.getByTestId("fields-table");
});
});

describe("Subfields view", () => {
it("Can navigate to a subfields view via URL", () => {
cy.visit("/dataset/demo_users_dataset/users/workplace_info");
cy.getByTestId("fields-table");
});

it("Displays a table with the subfields", () => {
cy.visit("/dataset/demo_users_dataset/users/workplace_info");
cy.getByTestId("fields-table");
cy.getByTestId("row-0-col-name").contains("employer");
cy.getByTestId("row-1-col-name").contains("position");
cy.getByTestId("row-2-col-name").contains("direct_reports");
});

it("Can use the search bar to filter subfields", () => {
cy.visit("/dataset/demo_users_dataset/users/workplace_info");
cy.getByTestId("fields-table");
cy.getByTestId("fields-search").type("position");
cy.getByTestId("row-0-col-name").contains("position");
cy.getByTestId("row-1-col-name").should("not.exist");
});

it("Can navigate to a level deeper in nested fields", () => {
cy.visit("/dataset/demo_users_dataset/users/workplace_info");
cy.getByTestId("row-0-col-name").contains("employer").click();
cy.url().should(
"contain",
"/dataset/demo_users_dataset/users/workplace_info.employer",
);
cy.getByTestId("fields-table");
cy.getByTestId("row-0-col-name").contains("name");
cy.getByTestId("row-1-col-name").contains("address");
cy.getByTestId("row-2-col-name").contains("phone");
});
});

describe("Creating datasets", () => {
Expand Down
110 changes: 110 additions & 0 deletions clients/admin-ui/cypress/fixtures/dataset.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,116 @@
"description": "User's unique ID",
"data_categories": ["user.unique_id"],
"fields": null
},
{
"name": "workplace_info",
"description": null,
"data_categories": null,
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "object",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": [
{
"name": "employer",
"description": null,
"data_categories": null,
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "object",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": [
{
"name": "name",
"description": null,
"data_categories": ["user.name"],
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "string",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": null
},
{
"name": "address",
"description": null,
"data_categories": ["user.childrens"],
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "string",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": null
},
{
"name": "phone",
"description": "",
"data_categories": [
"user.financial.credit_card",
"user.contact.address.city"
],
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "string",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": null
}
]
},
{
"name": "position",
"description": "",
"data_categories": ["user.content"],
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "string",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": null
},
{
"name": "direct_reports",
"description": "anotherdescription",
"data_categories": ["user.name"],
"fides_meta": {
"references": null,
"identity": null,
"primary_key": null,
"data_type": "string[]",
"length": null,
"return_all_elements": null,
"read_only": null
},
"fields": null
}
]
}
]
},
Expand Down
35 changes: 14 additions & 21 deletions clients/admin-ui/src/features/common/EditDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,16 @@ interface Props {
footer?: ReactNode;
}

export const EditDrawerHeader = ({
title,
onDelete,
}: {
title: string;
onDelete?: () => void;
}) => (
export const EditDrawerHeader = ({ title }: { title: string }) => (
<DrawerHeader py={0} display="flex" alignItems="flex-start">
<Text mr="2" color="gray.700" fontSize="lg" lineHeight={1.8}>
{title}
</Text>
{onDelete ? (
<IconButton
variant="outline"
aria-label="delete"
icon={<TrashCanOutlineIcon fontSize="small" />}
size="sm"
onClick={onDelete}
data-testid="delete-btn"
/>
) : null}
</DrawerHeader>
);

export const EditDrawerFooter = ({
onClose,
onDelete,
formId,
isSaving,
}: {
Expand All @@ -59,11 +43,20 @@ export const EditDrawerFooter = ({
*/
formId?: string;
isSaving?: boolean;
onDelete?: () => void;
} & Pick<Props, "onClose">) => (
<DrawerFooter justifyContent="space-between">
<Button onClick={onClose} mr={2} size="sm" variant="outline">
Cancel
</Button>
{onDelete ? (
<IconButton
variant="outline"
aria-label="delete"
icon={<TrashCanOutlineIcon fontSize="small" />}
size="sm"
onClick={onDelete}
data-testid="delete-btn"
/>
) : null}

<Button
type="submit"
colorScheme="primary"
Expand Down
9 changes: 7 additions & 2 deletions clients/admin-ui/src/features/common/nav/v2/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]/[subfieldUrn]";

// Detection and discovery
export const DETECTION_DISCOVERY_ACTIVITY_ROUTE = "/data-discovery/activity";
Expand Down
Loading
Loading