Skip to content

Commit

Permalink
Classify data category dropdown (#1110)
Browse files Browse the repository at this point in the history
* Refactor DataCategoryInput so it can be reused

* Initial ClassifiedDataCategoryInput

* Handle change from classified select

* Add remove logic

* Add playground

* Rename ClassifiedDataCategoryInput --> ClassifiedDataCategoryDropdown

* Update changelog

* Render fides_key instead of name

* Pull DataCategoryInput into its own file

It is getting big, and it avoids circular dependencies

* Wrap cypress component testing in redux provider wrapper so that components accessing the store will still work

* Update tests for DataCategoryInput to handle classified version

* Enable rendering ClassifiedDataCategoryDropdown when plus is available

* Allow categories without confidences to show up in select box

* Fixed confidence precision in category dropdown

* sq merg

Co-authored-by: Sebastian Sangervasi <sebastian@ethyca.com>
  • Loading branch information
allisonking and ssangervasi authored Oct 3, 2022
1 parent e82fe92 commit ac92cb6
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 121 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The types of changes are:
* Added toggle for enabling classify during generation. [#1057](https://github.com/ethyca/fides/pull/1057)
* Initial implementation of API request to kick off classify, with confirmation modal. [#1069](https://github.com/ethyca/fides/pull/1069)
* Initial Classification & Review status for generated datasets. [#1074](https://github.com/ethyca/fides/pull/1074)
* Component for choosing data categories based on classification results. [#1110](https://github.com/ethyca/fides/pull/1110)
* The dataset fields table shows data categories from the classifier (if available). [#1088](https://github.com/ethyca/fides/pull/1088)
* The "Approve" button can be used to update the dataset with the classifier's suggestions. [#1129](https://github.com/ethyca/fides/pull/1129)
* System management UI:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import * as React from "react";

import ClassifiedDataCategoryDropdown from "~/features/dataset/ClassifiedDataCategoryDropdown";
import { MOCK_DATA_CATEGORIES } from "~/mocks/data";
import { DataCategory } from "~/types/api";

const CATEGORIES_WITH_CONFIDENCE = MOCK_DATA_CATEGORIES.map((dc) => ({
...dc,
confidence: Math.floor(Math.random() * 100),
}));
const MOST_LIKELY_CATEGORIES = CATEGORIES_WITH_CONFIDENCE.slice(0, 5);

describe("ClassifiedDataCategoryDropdown", () => {
it("can render checked categories", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
const selectedClassifiedCategory = MOST_LIKELY_CATEGORIES[0];
cy.mount(
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
// check one "most likely" and one regular one
checked={[
selectedClassifiedCategory.fides_key,
"system.authentication",
]}
onChecked={onCheckedSpy}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);

// check that the classified one is selected
cy.getByTestId("classified-select").should(
"contain",
selectedClassifiedCategory.fides_key
);
// check that the regular one is selected
cy.getByTestId("data-category-dropdown").click();
cy.get("[data-testid='checkbox-Authentication Data'] > span").should(
"have.attr",
"data-checked"
);
cy.get(
`[data-testid='checkbox-${selectedClassifiedCategory.name}'] > span`
).should("have.attr", "data-checked");
});

it("can render all categories even if not recommended", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
const selectedClassifiedCategory = MOST_LIKELY_CATEGORIES[0];
const otherCategory = MOCK_DATA_CATEGORIES[6];
cy.mount(
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
// check one "most likely" and one regular one
checked={[
selectedClassifiedCategory.fides_key,
"system.authentication",
]}
onChecked={onCheckedSpy}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);
cy.getByTestId("classified-select").contains("system.authentication (N/A)");
cy.getByTestId("classified-select")
.click()
.within(() => {
cy.contains(`${otherCategory.fides_key} (N/A)`);
});
});

it("can select from classified select without overriding taxonomy dropdown", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
const toSelect = MOST_LIKELY_CATEGORIES[0];
cy.mount(
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
checked={["system.authentication"]}
onChecked={onCheckedSpy}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);
cy.getByTestId("classified-select")
.click()
.type(`${toSelect.fides_key}{enter}`);
cy.get("@onCheckedSpy").should("have.been.calledWith", [
"system.authentication",
toSelect.fides_key,
]);
});

it("can select from taxonomy dropdown without overriding classified select", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
const selectedClassifiedCategory = MOST_LIKELY_CATEGORIES[0];
cy.mount(
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
checked={[selectedClassifiedCategory.fides_key]}
onChecked={onCheckedSpy}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);
cy.getByTestId("data-category-dropdown").click();
cy.getByTestId("expand-System Data").click();
cy.getByTestId("checkbox-Authentication Data").click();
cy.get("@onCheckedSpy").should("have.been.calledWith", [
selectedClassifiedCategory.fides_key,
"system.authentication",
]);
});

it("can remove items from classified select", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
const selectedClassifiedCategory = MOST_LIKELY_CATEGORIES[0];
cy.mount(
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
checked={[
selectedClassifiedCategory.fides_key,
"system.authentication",
]}
onChecked={onCheckedSpy}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);
// delete the selected category
cy.getByTestId("classified-select").click().type("{backspace}");
cy.get("@onCheckedSpy").should("have.been.calledWith", [
selectedClassifiedCategory.fides_key,
]);
});

it("can remove items from taxonomy select", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
const selectedClassifiedCategory = MOST_LIKELY_CATEGORIES[0];
cy.mount(
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
checked={[
selectedClassifiedCategory.fides_key,
"system.authentication",
]}
onChecked={onCheckedSpy}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);
// uncheck system authentication
cy.getByTestId("data-category-dropdown").click();
cy.getByTestId("checkbox-Authentication Data").click();
cy.get("@onCheckedSpy").should("have.been.calledWith", [
selectedClassifiedCategory.fides_key,
]);
});

it("playground", () => {
// it's useful when developing to be able to play with the component with actual react state
const ClassifiedDataCategoryDropdownWithState = () => {
const [checked, setChecked] = React.useState([]);
return (
<ClassifiedDataCategoryDropdown
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
checked={checked}
onChecked={setChecked}
mostLikelyCategories={MOST_LIKELY_CATEGORIES}
/>
);
};
cy.mount(<ClassifiedDataCategoryDropdownWithState />);
});
});
18 changes: 18 additions & 0 deletions clients/ctl/admin-ui/cypress/components/DataCategoryInput.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import DataCategoryInput from "~/features/dataset/DataCategoryInput";
import { MOCK_DATA_CATEGORIES } from "~/mocks/data";
import { DataCategory } from "~/types/api";

import { stubPlus } from "../support/stubs";

describe("DataCategoryInput", () => {
it("can check a category", () => {
const onCheckedSpy = cy.spy().as("onCheckedSpy");
Expand All @@ -15,6 +17,8 @@ describe("DataCategoryInput", () => {
/>
);

cy.getByTestId("classified-select").should("not.exist");

cy.getByTestId("selected-categories").should("contain", "user");
cy.getByTestId("data-category-dropdown").click();
cy.getByTestId("expand-System Data").click();
Expand All @@ -25,4 +29,18 @@ describe("DataCategoryInput", () => {
"system.authentication",
]);
});

it("can render the classified version", () => {
stubPlus(true);

const onCheckedSpy = cy.spy().as("onCheckedSpy");
cy.mount(
<DataCategoryInput
dataCategories={MOCK_DATA_CATEGORIES as DataCategory[]}
checked={["user"]}
onChecked={onCheckedSpy}
/>
);
cy.getByTestId("classified-select");
});
});
36 changes: 27 additions & 9 deletions clients/ctl/admin-ui/cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ import "@fontsource/inter/500.css";
import "@fontsource/inter/700.css";

import { ChakraProvider } from "@chakra-ui/react";
import { EnhancedStore } from "@reduxjs/toolkit";
// Alternatively you can use CommonJS syntax:
// require('./commands')
// eslint-disable-next-line import/no-extraneous-dependencies
import { mount } from "cypress/react";
import { mount, MountOptions, MountReturn } from "cypress/react";
import * as React from "react";
import { Provider } from "react-redux";

import theme from "../../src/theme";
import { AppState, makeStore } from "~/app/store";
import theme from "~/theme";

// Augment the Cypress namespace to include type definitions for
// your custom command.
Expand All @@ -35,14 +38,29 @@ import theme from "../../src/theme";
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
/**
* Mounts a React node
* @param component React Node to mount
* @param options Additional options to pass into mount
*/
mount(
component: React.ReactNode,
options?: MountOptions & { reduxStore?: EnhancedStore<AppState> }
): Cypress.Chainable<MountReturn>;
}
}
}

Cypress.Commands.add("mount", (jsx, options) =>
mount(React.createElement(ChakraProvider, { theme }, jsx), options)
);

// Example use:
// cy.mount(<MyComponent />)
/**
* Wrap the default mount in Redux and Chakra
*/
Cypress.Commands.add("mount", (component, options = {}) => {
const { reduxStore = makeStore(), ...mountOptions } = options;
const wrapChakra = React.createElement(ChakraProvider, { theme }, component);
const wrapRedux = React.createElement(
Provider,
{ store: reduxStore },
wrapChakra
);
return mount(wrapRedux, mountOptions);
});
2 changes: 1 addition & 1 deletion clients/ctl/admin-ui/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { reducer as systemReducer, systemApi } from "~/features/system";
import { reducer as taxonomyReducer, taxonomyApi } from "~/features/taxonomy";
import { reducer as userReducer } from "~/features/user";

const makeStore = () => {
export const makeStore = () => {
const store = configureStore({
reducer: {
configWizard: configWizardReducer,
Expand Down
Loading

0 comments on commit ac92cb6

Please sign in to comment.