Skip to content

Commit dd2f04e

Browse files
committed
Add UI for custom taxonomy CRUD (#6921)
1 parent 72a6834 commit dd2f04e

File tree

16 files changed

+558
-26
lines changed

16 files changed

+558
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
3434
- Added duplicate DSR checking to request runner [#6860](https://github.com/ethyca/fides/pull/6860/)
3535
- Added keyboard shortcuts for field actions in Action Center [#6919](https://github.com/ethyca/fides/pull/6919)
3636
- Added keyboard shortcuts helper modal in Action Center fields [#6926](https://github.com/ethyca/fides/pull/6926)
37+
- Added UI for configuring custom taxonomies [#6921](https://github.com/ethyca/fides/pull/6921)
3738

3839
### Changed
3940
- Restricted monitor tree selection to only classifiable resource types [#6900](https://github.com/ethyca/fides/pull/6900)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
AntButton as Button,
3+
AntFlex as Flex,
4+
AntForm as Form,
5+
AntInput as Input,
6+
AntMessageInstance as MessageInstance,
7+
AntTypography as Typography,
8+
} from "fidesui";
9+
10+
import { getErrorMessage, isErrorResult } from "~/features/common/helpers";
11+
import { formatKey } from "~/features/datastore-connections/system_portal_config/helpers";
12+
import { useCreateCustomTaxonomyMutation } from "~/features/taxonomy/taxonomy.slice";
13+
14+
interface CreateCustomTaxonomyFormValues {
15+
name: string;
16+
description: string;
17+
}
18+
19+
const FORM_COPY =
20+
"Create and organize the core terminology your team uses to describe data and how it's handled. Custom taxonomies let you define domain-specific concepts and then apply them to other privacy objects throughout the platform";
21+
22+
const CreateCustomTaxonomyForm = ({
23+
onClose,
24+
messageApi,
25+
}: {
26+
onClose: () => void;
27+
messageApi: MessageInstance;
28+
}) => {
29+
const [createCustomTaxonomy, { isLoading: isSubmitting }] =
30+
useCreateCustomTaxonomyMutation();
31+
32+
const handleSubmit = async (values: CreateCustomTaxonomyFormValues) => {
33+
const payload = {
34+
...values,
35+
taxonomy_type: formatKey(values.name ?? ""),
36+
};
37+
const result = await createCustomTaxonomy(payload);
38+
if (isErrorResult(result)) {
39+
messageApi.error(getErrorMessage(result.error));
40+
return;
41+
}
42+
messageApi.success("Taxonomy created successfully");
43+
onClose();
44+
};
45+
46+
return (
47+
<>
48+
<Typography.Paragraph>{FORM_COPY}</Typography.Paragraph>
49+
<Form
50+
layout="vertical"
51+
onFinish={handleSubmit}
52+
validateTrigger={["onBlur", "onChange"]}
53+
>
54+
<Form.Item
55+
label="Taxonomy name"
56+
name="name"
57+
rules={[{ required: true, message: "Name is required" }]}
58+
>
59+
<Input data-testid="input-name" />
60+
</Form.Item>
61+
62+
<Form.Item label="Description" name="description">
63+
<Input.TextArea rows={2} data-testid="input-description" />
64+
</Form.Item>
65+
66+
<Flex gap="small" justify="space-between">
67+
<Button onClick={onClose} disabled={isSubmitting}>
68+
Cancel
69+
</Button>
70+
<Button
71+
type="primary"
72+
htmlType="submit"
73+
loading={isSubmitting}
74+
disabled={isSubmitting}
75+
data-testid="save-btn"
76+
>
77+
Create taxonomy
78+
</Button>
79+
</Flex>
80+
</Form>
81+
</>
82+
);
83+
};
84+
85+
export default CreateCustomTaxonomyForm;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
AntDescriptions as Descriptions,
3+
AntFlex as Flex,
4+
AntForm as Form,
5+
AntInput as Input,
6+
} from "fidesui";
7+
8+
import { TaxonomyResponse } from "~/types/api/models/TaxonomyResponse";
9+
import { TaxonomyUpdate } from "~/types/api/models/TaxonomyUpdate";
10+
11+
interface CustomTaxonomyDetailsProps {
12+
taxonomy?: TaxonomyResponse | null;
13+
onSubmit: (values: TaxonomyUpdate) => void;
14+
formId: string;
15+
}
16+
17+
const CustomTaxonomyDetails = ({
18+
taxonomy,
19+
onSubmit,
20+
formId,
21+
}: CustomTaxonomyDetailsProps) => {
22+
if (!taxonomy) {
23+
return null;
24+
}
25+
const { fides_key: fidesKey, ...initialValues } = taxonomy;
26+
27+
return (
28+
<Flex vertical gap="large">
29+
<Descriptions>
30+
<Descriptions.Item label="Fides key">{fidesKey}</Descriptions.Item>
31+
</Descriptions>
32+
<Form
33+
id={formId}
34+
layout="vertical"
35+
initialValues={initialValues}
36+
onFinish={onSubmit}
37+
>
38+
<Form.Item
39+
label="Name"
40+
name="name"
41+
rules={[{ required: true, message: "Name is required" }]}
42+
>
43+
<Input />
44+
</Form.Item>
45+
<Form.Item label="Description" name="description">
46+
<Input.TextArea />
47+
</Form.Item>
48+
</Form>
49+
</Flex>
50+
);
51+
};
52+
53+
export default CustomTaxonomyDetails;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
AntButton as Button,
3+
AntFlex as Flex,
4+
AntMessageInstance as MessageInstance,
5+
} from "fidesui";
6+
7+
import { getErrorMessage, isErrorResult } from "~/features/common/helpers";
8+
import { DetailsDrawer } from "~/features/data-discovery-and-detection/action-center/fields/DetailsDrawer";
9+
import { DetailsDrawerProps } from "~/features/data-discovery-and-detection/action-center/fields/DetailsDrawer/types";
10+
import CustomTaxonomyDetails from "~/features/taxonomy/components/CustomTaxonomyDetails";
11+
import { useUpdateCustomTaxonomyMutation } from "~/features/taxonomy/taxonomy.slice";
12+
import { TaxonomyResponse } from "~/types/api/models/TaxonomyResponse";
13+
import { TaxonomyUpdate } from "~/types/api/models/TaxonomyUpdate";
14+
15+
interface CustomTaxonomyEditDrawerProps
16+
extends Omit<DetailsDrawerProps, "itemKey"> {
17+
taxonomy?: TaxonomyResponse | null;
18+
onDelete: () => void;
19+
messageApi: MessageInstance;
20+
}
21+
22+
const FORM_ID = "custom-taxonomy-form";
23+
24+
const CustomTaxonomyEditDrawer = ({
25+
taxonomy,
26+
onClose,
27+
onDelete,
28+
messageApi,
29+
...props
30+
}: CustomTaxonomyEditDrawerProps) => {
31+
const [updateCustomTaxonomy, { isLoading: isUpdating }] =
32+
useUpdateCustomTaxonomyMutation();
33+
34+
const handleUpdate = async (values: TaxonomyUpdate) => {
35+
if (!taxonomy?.fides_key) {
36+
messageApi.error("Taxonomy not found");
37+
return;
38+
}
39+
const result = await updateCustomTaxonomy({
40+
fides_key: taxonomy.fides_key,
41+
...values,
42+
});
43+
if (isErrorResult(result)) {
44+
messageApi.error(getErrorMessage(result.error));
45+
return;
46+
}
47+
messageApi.success("Taxonomy updated successfully");
48+
};
49+
50+
return (
51+
<DetailsDrawer
52+
title={`Edit ${taxonomy?.name}`}
53+
{...props}
54+
itemKey=""
55+
open={!!taxonomy}
56+
onClose={onClose}
57+
destroyOnHidden
58+
footer={
59+
<Flex justify="space-between" className="w-full">
60+
<Button onClick={onDelete}>Delete</Button>
61+
<Button
62+
type="primary"
63+
htmlType="submit"
64+
form={FORM_ID}
65+
loading={isUpdating}
66+
>
67+
Save
68+
</Button>
69+
</Flex>
70+
}
71+
>
72+
<CustomTaxonomyDetails
73+
taxonomy={taxonomy}
74+
onSubmit={handleUpdate}
75+
formId={FORM_ID}
76+
/>
77+
</DetailsDrawer>
78+
);
79+
};
80+
81+
export default CustomTaxonomyEditDrawer;

clients/admin-ui/src/features/taxonomy/components/TaxonomyEditDrawer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface TaxonomyEditDrawerProps {
3333
onClose: () => void;
3434
}
3535

36-
const TaxonomyEditDrawer = ({
36+
const TaxonomyItemEditDrawer = ({
3737
taxonomyItem,
3838
taxonomyType,
3939
onClose: closeDrawer,
@@ -210,4 +210,4 @@ const TaxonomyEditDrawer = ({
210210
</>
211211
);
212212
};
213-
export default TaxonomyEditDrawer;
213+
export default TaxonomyItemEditDrawer;

clients/admin-ui/src/features/taxonomy/components/TaxonomyInteractiveTree.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ import {
1616
import palette from "fidesui/src/palette/palette.module.scss";
1717
import { useEffect, useMemo } from "react";
1818

19-
import {
20-
TAXONOMY_ROOT_NODE_ID,
21-
taxonomyTypeToLabel,
22-
} from "~/features/taxonomy/constants";
19+
import { TAXONOMY_ROOT_NODE_ID } from "~/features/taxonomy/constants";
2320
import { TaxonomyTreeHoverProvider } from "~/features/taxonomy/context/TaxonomyTreeHoverContext";
2421
import useD3HierarchyLayout from "~/features/taxonomy/hooks/useD3HierarchyLayout";
2522
import { TaxonomyEntity } from "~/features/taxonomy/types";
@@ -36,12 +33,14 @@ interface TaxonomyInteractiveTreeProps {
3633
draftNewItem: Partial<TaxonomyEntity> | null;
3734
lastCreatedItemKey: string | null;
3835
resetLastCreatedItemKey: () => void;
36+
onRootItemClick: (() => void) | null;
3937
onTaxonomyItemClick: (taxonomyItem: TaxonomyEntity) => void;
4038
onAddButtonClick: (taxonomyItem: TaxonomyEntity | undefined) => void;
4139
onCancelDraftItem: () => void;
4240
onSubmitDraftItem: (label: string) => void;
4341
userCanAddLabels: boolean;
4442
isCreating?: boolean;
43+
rootNodeLabel?: string;
4544
}
4645

4746
const TaxonomyInteractiveTree = ({
@@ -50,12 +49,14 @@ const TaxonomyInteractiveTree = ({
5049
draftNewItem,
5150
lastCreatedItemKey,
5251
resetLastCreatedItemKey,
52+
onRootItemClick,
5353
onTaxonomyItemClick,
5454
onAddButtonClick,
5555
onCancelDraftItem,
5656
onSubmitDraftItem,
5757
userCanAddLabels,
5858
isCreating = false,
59+
rootNodeLabel,
5960
}: TaxonomyInteractiveTreeProps) => {
6061
const { fitView } = useReactFlow();
6162

@@ -72,19 +73,26 @@ const TaxonomyInteractiveTree = ({
7273
id: TAXONOMY_ROOT_NODE_ID,
7374
position: { x: 0, y: 0 },
7475
data: {
75-
label: taxonomyTypeToLabel(taxonomyType),
76+
label: rootNodeLabel || taxonomyType,
7677
taxonomyItem: {
7778
fides_key: TAXONOMY_ROOT_NODE_ID,
7879
},
7980
taxonomyType,
80-
onTaxonomyItemClick: null,
81+
onTaxonomyItemClick: onRootItemClick,
8182
onAddButtonClick,
8283
hasChildren: taxonomyItems.length !== 0,
8384
userCanAddLabels,
8485
},
8586
type: "taxonomyTreeNode",
8687
}),
87-
[taxonomyType, taxonomyItems.length, onAddButtonClick, userCanAddLabels],
88+
[
89+
rootNodeLabel,
90+
taxonomyType,
91+
onRootItemClick,
92+
onAddButtonClick,
93+
taxonomyItems.length,
94+
userCanAddLabels,
95+
],
8896
);
8997

9098
const nodes: Node[] = [rootNode];

clients/admin-ui/src/features/taxonomy/taxonomy.slice.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { baseApi } from "~/features/common/api.slice";
2+
import { TaxonomyCreate } from "~/types/api/models/TaxonomyCreate";
3+
import { TaxonomyResponse } from "~/types/api/models/TaxonomyResponse";
4+
import { TaxonomyUpdate } from "~/types/api/models/TaxonomyUpdate";
25

36
import { TaxonomyEntity } from "./types";
47

58
type TaxonomySummary = { fides_key: string; name: string };
69

710
const taxonomyApi = baseApi.injectEndpoints({
811
endpoints: (build) => ({
9-
getCustomTaxonomies: build.query<TaxonomySummary[], void>({
10-
query: () => ({ url: `taxonomies` }),
11-
}),
1212
getTaxonomy: build.query<TaxonomyEntity[], string>({
1313
query: (taxonomyType) => ({ url: `taxonomies/${taxonomyType}/elements` }),
1414
providesTags: (result, error, taxonomyType) => [
@@ -47,6 +47,10 @@ const taxonomyApi = baseApi.injectEndpoints({
4747
return result;
4848
},
4949
}),
50+
getCustomTaxonomies: build.query<TaxonomySummary[], void>({
51+
query: () => ({ url: `taxonomies` }),
52+
providesTags: () => [{ type: "Taxonomy" }],
53+
}),
5054
createTaxonomy: build.mutation<
5155
TaxonomyEntity,
5256
{ taxonomyType: string } & Partial<TaxonomyEntity>
@@ -86,6 +90,32 @@ const taxonomyApi = baseApi.injectEndpoints({
8690
{ type: "Taxonomy", id: taxonomyType },
8791
],
8892
}),
93+
createCustomTaxonomy: build.mutation<TaxonomyResponse, TaxonomyCreate>({
94+
query: (body) => ({
95+
url: `taxonomies`,
96+
method: "POST",
97+
body,
98+
}),
99+
invalidatesTags: () => [{ type: "Taxonomy" }],
100+
}),
101+
updateCustomTaxonomy: build.mutation<
102+
TaxonomyResponse,
103+
TaxonomyUpdate & { fides_key: string }
104+
>({
105+
query: ({ fides_key, ...body }) => ({
106+
url: `taxonomies/${fides_key}`,
107+
method: "PUT",
108+
body,
109+
}),
110+
invalidatesTags: () => [{ type: "Taxonomy" }],
111+
}),
112+
deleteCustomTaxonomy: build.mutation<void, string>({
113+
query: (fides_key) => ({
114+
url: `taxonomies/${fides_key}`,
115+
method: "DELETE",
116+
}),
117+
invalidatesTags: () => [{ type: "Taxonomy" }],
118+
}),
89119
}),
90120
});
91121

@@ -95,4 +125,7 @@ export const {
95125
useCreateTaxonomyMutation,
96126
useUpdateTaxonomyMutation,
97127
useDeleteTaxonomyMutation,
128+
useCreateCustomTaxonomyMutation,
129+
useUpdateCustomTaxonomyMutation,
130+
useDeleteCustomTaxonomyMutation,
98131
} = taxonomyApi;

0 commit comments

Comments
 (0)