Skip to content

Commit

Permalink
Add taxonomy entity form (#1019)
Browse files Browse the repository at this point in the history
* Rename data-categories.slice --> taxonomy.slice

* Add active taxonomy type to store

* Add create mutation to slices

* Hook up create to form

* Fixup form UX

* Derive parent key from fides key

* Conditionally render parent key field

* Add tests for adding taxonomy entities

* Clean up

* Update changelog

* Simplify setting add state

* Add test for showing either add or create form

* Derive isCreate from status of fides key
  • Loading branch information
allisonking authored Aug 26, 2022
1 parent 1d7b896 commit ba09cec
Show file tree
Hide file tree
Showing 20 changed files with 437 additions and 106 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ The types of changes are:

* Added the ability to edit taxonomy fields via the UI [#977](https://github.com/ethyca/fides/pull/977)
* New column `is_default` added to DataCategory, DataUse, DataSubject, and DataQualifier tables [#976](https://github.com/ethyca/fides/pull/976)
* Added the ability to add taxonomy fields via the UI [#1019](https://github.com/ethyca/fides/pull/1019)
* Added the ability to delete taxonomy fields via the UI [#1006](https://github.com/ethyca/fides/pull/1006)
* Prevent modifying taxonomy `is_default` fields and from adding `is_default=True` fields via the API [#990](https://github.com/ethyca/fides/pull/990).

### Changed

* Upgraded base Docker version to Python 3.9 and updated all other references from 3.8 -> 3.9 [#974](https://github.com/ethyca/fides/pull/974)
Expand Down
18 changes: 18 additions & 0 deletions clients/ctl/admin-ui/__tests__/features/taxonomy-helpers.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { parentKeyFromFidesKey } from "~/features/taxonomy/helpers";

describe("taxonomy helpers", () => {
describe("parent key from fides key", () => {
it("should handle nested fides keys", () => {
const fidesKey = "grandparent.parent.child";
expect(parentKeyFromFidesKey(fidesKey)).toEqual("grandparent.parent");
});
it("should handle empty fides key", () => {
const fidesKey = "";
expect(parentKeyFromFidesKey(fidesKey)).toEqual("");
});
it("should handle single fides key", () => {
const fidesKey = "root";
expect(parentKeyFromFidesKey(fidesKey)).toEqual("");
});
});
});
168 changes: 143 additions & 25 deletions clients/ctl/admin-ui/cypress/e2e/taxonomy.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,20 @@ describe("Taxonomy management page", () => {
"have.value",
tabValue.description
);
cy.getByTestId("input-parent_key").should(
"have.value",
tabValue.parentKey
);
cy.getByTestId("input-parent_key").should("be.disabled");
cy.getByTestId("update-btn").should("be.disabled");
if (tabValue.tab !== "Data Subjects") {
cy.getByTestId("input-parent_key").should(
"have.value",
tabValue.parentKey
);
cy.getByTestId("input-parent_key").should("be.disabled");
}
cy.getByTestId("submit-btn").should("be.disabled");

// make an edit
const addedText = "foo";
cy.getByTestId("input-name").type(addedText);
cy.getByTestId("update-btn").should("be.enabled");
cy.getByTestId("update-btn").click();
cy.getByTestId("submit-btn").should("be.enabled");
cy.getByTestId("submit-btn").click();
cy.wait(tabValue.request).then((interception) => {
const { body } = interception.request;
expect(body.name).to.eql(`${tabValue.name}${addedText}`);
Expand Down Expand Up @@ -207,7 +209,7 @@ describe("Taxonomy management page", () => {
cy.getByTestId("input-legitimate_interest_impact_assessment")
.clear()
.type("foo");
cy.getByTestId("update-btn").click();
cy.getByTestId("submit-btn").click();
cy.wait("@putDataUse").then((interception) => {
const { body } = interception.request;
const expected = {
Expand All @@ -216,7 +218,6 @@ describe("Taxonomy management page", () => {
description:
"Provide, give, or make available the product, service, application or system.",
is_default: true,
parent_key: null,
legal_basis: "Legitimate Interests",
special_category: "Vital Interests",
recipients: ["marketing team", "dog shelter"],
Expand Down Expand Up @@ -260,7 +261,7 @@ describe("Taxonomy management page", () => {

// trigger a PUT
cy.getByTestId("input-name").clear().type("foo");
cy.getByTestId("update-btn").click();
cy.getByTestId("submit-btn").click();
cy.wait("@putDataSubject").then((interception) => {
const { body } = interception.request;
const expected = {
Expand Down Expand Up @@ -298,14 +299,140 @@ describe("Taxonomy management page", () => {

const addedText = "foo";
cy.getByTestId("input-name").type(addedText);
cy.getByTestId("update-btn").click();
cy.getByTestId("submit-btn").click();

cy.wait("@putDataCategoryError");
cy.getByTestId("toast-success-msg").should("not.exist");
cy.getByTestId("taxonomy-form-error").should("contain", errorMsg);
});
});

describe("Can create data", () => {
beforeEach(() => {
cy.visit("/taxonomy");
const taxonomyPayload = {
statusCode: 200,
body: {
fides_key: "key",
organization_fides_key: "default_organization",
name: "name",
description: "description",
parent_key: undefined,
},
};
cy.intercept("POST", "/api/v1/data_category*", taxonomyPayload).as(
"postDataCategory"
);
cy.intercept("POST", "/api/v1/data_use*", taxonomyPayload).as(
"postDataUse"
);
cy.intercept("POST", "/api/v1/data_subject*", taxonomyPayload).as(
"postDataSubject"
);
cy.intercept("POST", "/api/v1/data_qualifier*", taxonomyPayload).as(
"postDataQualifier"
);
});

it("Can open a create form for each taxonomy entity", () => {
const expectedTabValues = [
{
tab: "Data Categories",
name: "Data category",
request: "@postDataCategory",
},
{
tab: "Data Uses",
name: "Data use",
request: "@postDataUse",
},
{
tab: "Data Subjects",
name: "Data subject",
request: "@postDataSubject",
},
{
tab: "Identifiability",
name: "Data qualifier",
request: "@postDataQualifier",
},
];
expectedTabValues.forEach((tabValue) => {
cy.getByTestId(`tab-${tabValue.tab}`).click();
cy.getByTestId("add-taxonomy-btn").click();
cy.getByTestId("create-taxonomy-form");
cy.getByTestId("form-heading").should("contain", tabValue.name);

// add a root value
cy.getByTestId("input-fides_key").type("foo");
if (tabValue.tab !== "Data Subjects") {
cy.getByTestId("input-parent_key").should("have.value", "");
}
cy.getByTestId("submit-btn").click();
cy.wait(tabValue.request).then((interception) => {
const { body } = interception.request;
expect(body.fides_key).to.eql("foo");
expect(body.parent_key).to.equal(undefined);
expect(body.is_default).to.equal(false);
});
cy.getByTestId("toast-success-msg").should("exist");

// add a child value
cy.getByTestId("add-taxonomy-btn").click();
cy.getByTestId("input-fides_key").type("foo.bar.baz");
if (tabValue.tab !== "Data Subjects") {
cy.getByTestId("input-parent_key").should("have.value", "foo.bar");
}
cy.getByTestId("submit-btn").click();
cy.wait(tabValue.request).then((interception) => {
const { body } = interception.request;
expect(body.fides_key).to.eql("foo.bar.baz");
expect(body.parent_key).to.equal("foo.bar");
expect(body.is_default).to.equal(false);
});
cy.getByTestId("toast-success-msg").should("exist");
});
});

it("Can trigger an error", () => {
const errorMsg = "Internal Server Error";
cy.intercept("POST", "/api/v1/data_category*", {
statusCode: 500,
body: errorMsg,
}).as("postDataCategoryError");

cy.getByTestId(`tab-Data Categories`).click();
cy.getByTestId("add-taxonomy-btn").click();

cy.getByTestId("input-fides_key").type("foo");
cy.getByTestId("submit-btn").click();

cy.wait("@postDataCategoryError");
cy.getByTestId("toast-success-msg").should("not.exist");
cy.getByTestId("taxonomy-form-error").should("contain", errorMsg);
});

it("Will only show either the add or the edit form", () => {
cy.getByTestId(`tab-Data Categories`).click();
const openEditForm = () => {
cy.getByTestId("accordion-item-System Data").trigger("mouseover");
cy.getByTestId("edit-btn").click();
};
const openCreateForm = () => {
cy.getByTestId("add-taxonomy-btn").click();
};
openEditForm();
cy.getByTestId("edit-taxonomy-form");
cy.getByTestId("create-taxonomy-form").should("not.exist");
openCreateForm();
cy.getByTestId("edit-taxonomy-form").should("not.exist");
cy.getByTestId("create-taxonomy-form");
openEditForm();
cy.getByTestId("edit-taxonomy-form");
cy.getByTestId("create-taxonomy-form").should("not.exist");
});
});

describe("Can delete data", () => {
beforeEach(() => {
cy.visit("/taxonomy");
Expand Down Expand Up @@ -344,25 +471,19 @@ describe("Taxonomy management page", () => {
cy.getByTestId("accordion-item-User Data").trigger("mouseover");
cy.getByTestId("delete-btn").should("not.exist");
cy.getByTestId("accordion-item-User Data").click();
cy.getByTestId("accordion-item-User Provided Data").click();
cy.getByTestId("item-User Provided Non-Identifiable Data").trigger(
"mouseover"
);
cy.getByTestId("item-Biometric Data").trigger("mouseover");
cy.getByTestId("delete-btn");
});

it("Can delete a data category", () => {
cy.getByTestId(`tab-Data Categories`).click();
cy.getByTestId("accordion-item-User Data").click();
cy.getByTestId("accordion-item-User Provided Data").click();
cy.getByTestId("item-User Provided Non-Identifiable Data").trigger(
"mouseover"
);
cy.getByTestId("item-Biometric Data").trigger("mouseover");
cy.getByTestId("delete-btn").click();
cy.getByTestId("continue-btn").click();
cy.wait("@deleteDataCategory").then((interception) => {
const { url } = interception.request;
expect(url).to.contain("user.provided.nonidentifiable");
expect(url).to.contain("user.biometric");
});
cy.getByTestId("toast-success-msg");
});
Expand Down Expand Up @@ -416,10 +537,7 @@ describe("Taxonomy management page", () => {
}).as("deleteDataCategoryError");
cy.getByTestId(`tab-Data Categories`).click();
cy.getByTestId("accordion-item-User Data").click();
cy.getByTestId("accordion-item-User Provided Data").click();
cy.getByTestId("item-User Provided Non-Identifiable Data").trigger(
"mouseover"
);
cy.getByTestId("item-Biometric Data").trigger("mouseover");
cy.getByTestId("delete-btn").click();
cy.getByTestId("continue-btn").click();
cy.wait("@deleteDataCategoryError");
Expand Down
11 changes: 4 additions & 7 deletions clients/ctl/admin-ui/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import {
reducer as organizationReducer,
} from "~/features/organization";
import { systemApi } from "~/features/system";
import {
dataCategoriesApi,
reducer as dataCategoriesReducer,
} from "~/features/taxonomy";
import { reducer as taxonomyReducer, taxonomyApi } from "~/features/taxonomy";
import { reducer as userReducer } from "~/features/user";

const makeStore = () => {
Expand All @@ -34,7 +31,7 @@ const makeStore = () => {
configWizard: configWizardReducer,
user: userReducer,
dataset: datasetReducer,
dataCategories: dataCategoriesReducer,
taxonomy: taxonomyReducer,
dataQualifier: dataQualifierReducer,
dataSubjects: dataSubjectsReducer,
dataUse: dataUseReducer,
Expand All @@ -43,7 +40,7 @@ const makeStore = () => {
[organizationApi.reducerPath]: organizationApi.reducer,
[scannerApi.reducerPath]: scannerApi.reducer,
[systemApi.reducerPath]: systemApi.reducer,
[dataCategoriesApi.reducerPath]: dataCategoriesApi.reducer,
[taxonomyApi.reducerPath]: taxonomyApi.reducer,
[dataQualifierApi.reducerPath]: dataQualifierApi.reducer,
[dataSubjectsApi.reducerPath]: dataSubjectsApi.reducer,
[dataUseApi.reducerPath]: dataUseApi.reducer,
Expand All @@ -54,7 +51,7 @@ const makeStore = () => {
organizationApi.middleware,
scannerApi.middleware,
systemApi.middleware,
dataCategoriesApi.middleware,
taxonomyApi.middleware,
dataQualifierApi.middleware,
dataSubjectsApi.middleware,
dataUseApi.middleware
Expand Down
2 changes: 1 addition & 1 deletion clients/ctl/admin-ui/src/features/common/DataTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@fidesui/react";
import { ReactNode } from "react";

interface TabData {
export interface TabData {
label: string;
content: ReactNode;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
selectDataCategories,
setDataCategories,
useGetAllDataCategoriesQuery,
} from "~/features/taxonomy/data-categories.slice";
} from "~/features/taxonomy/taxonomy.slice";
import { PrivacyDeclaration } from "~/types/api";

import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export const dataQualifierApi = createApi({
}),
invalidatesTags: ["Data Qualifiers"],
}),
createDataQualifier: build.mutation<DataQualifier, DataQualifier>({
query: (dataQualifier) => ({
url: `data_qualifier/`,
method: "POST",
body: dataQualifier,
}),
invalidatesTags: ["Data Qualifiers"],
}),
deleteDataQualifier: build.mutation<string, string>({
query: (key) => ({
url: `data_qualifier/${key}`,
Expand All @@ -52,6 +60,7 @@ export const dataQualifierApi = createApi({
export const {
useGetAllDataQualifiersQuery,
useUpdateDataQualifierMutation,
useCreateDataQualifierMutation,
useDeleteDataQualifierMutation,
} = dataQualifierApi;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export const dataSubjectsApi = createApi({
}),
invalidatesTags: ["Data Subjects"],
}),
createDataSubject: build.mutation<DataSubject, DataSubject>({
query: (dataSubject) => ({
url: `data_subject/`,
method: "POST",
body: dataSubject,
}),
invalidatesTags: ["Data Subjects"],
}),
deleteDataSubject: build.mutation<string, string>({
query: (key) => ({
url: `data_subject/${key}`,
Expand All @@ -51,6 +59,7 @@ export const dataSubjectsApi = createApi({
export const {
useGetAllDataSubjectsQuery,
useUpdateDataSubjectMutation,
useCreateDataSubjectMutation,
useDeleteDataSubjectMutation,
} = dataSubjectsApi;

Expand Down
9 changes: 9 additions & 0 deletions clients/ctl/admin-ui/src/features/data-use/data-use.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export const dataUseApi = createApi({
}),
invalidatesTags: ["Data Uses"],
}),
createDataUse: build.mutation<DataUse, DataUse>({
query: (dataUse) => ({
url: `data_use/`,
method: "POST",
body: dataUse,
}),
invalidatesTags: ["Data Uses"],
}),
deleteDataUse: build.mutation<string, string>({
query: (key) => ({
url: `data_use/${key}`,
Expand All @@ -51,6 +59,7 @@ export const dataUseApi = createApi({
export const {
useGetAllDataUsesQuery,
useUpdateDataUseMutation,
useCreateDataUseMutation,
useDeleteDataUseMutation,
} = dataUseApi;

Expand Down
Loading

0 comments on commit ba09cec

Please sign in to comment.