Skip to content

Commit

Permalink
Add dictionary data use UI (#4035)
Browse files Browse the repository at this point in the history
Co-authored-by: Kelsey Thomas <[email protected]>
  • Loading branch information
jpople and Kelsey-Ethyca authored Sep 7, 2023
1 parent 5989b5f commit 0e93f5a
Show file tree
Hide file tree
Showing 9 changed files with 543 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The types of changes are:
### Added

- Added new Performance-related nox commands and included them as part of the CI suite [#3997](https://github.com/ethyca/fides/pull/3997)
- Added dictionary suggestions for data uses [4035](https://github.com/ethyca/fides/pull/4035)

## [2.19.0](https://github.com/ethyca/fides/compare/2.18.0...2.19.0)

Expand Down
25 changes: 24 additions & 1 deletion clients/admin-ui/src/features/plus/plus.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from "~/types/api";
import { SystemHistoryResponse } from "~/types/api/models/SystemHistoryResponse";

import { DictEntry, Page } from "./types";
import { DictDataUse, DictEntry, Page } from "./types";

interface ScanParams {
classify?: boolean;
Expand Down Expand Up @@ -251,6 +251,17 @@ const plusApi = baseApi.injectEndpoints({
}),
providesTags: ["Dictionary"],
}),
getDictionaryDataUses: build.query<
Page<DictDataUse>,
{ vendor_id: number }
>({
query: ({ vendor_id }) => ({
params: { size: 1000 },
url: `plus/dictionary/data-use-declarations/${vendor_id}`,
method: "GET",
}),
providesTags: ["Dictionary"],
}),
getSystemHistory: build.query<
SystemHistoryResponse,
{ system_key: string }
Expand Down Expand Up @@ -283,6 +294,7 @@ export const {
useGetAllCustomFieldDefinitionsQuery,
useGetAllowListQuery,
useGetAllDictionaryEntriesQuery,
useGetDictionaryDataUsesQuery,
useGetSystemHistoryQuery,
} = plusApi;

Expand Down Expand Up @@ -425,3 +437,14 @@ export const selectDictEntry = (vendorId: string) =>
return dictEntry || EMPTY_DICT_ENTRY;
}
);

const EMPTY_DATA_USES: DictDataUse[] = [];

export const selectDictDataUses = (vendorId: number) =>
createSelector(
[
(state) => state,
plusApi.endpoints.getDictionaryDataUses.select({ vendor_id: vendorId }),
],
(state, { data }) => (data ? data.items : EMPTY_DATA_USES)
);
13 changes: 13 additions & 0 deletions clients/admin-ui/src/features/plus/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,16 @@ export type DictCookie = {
vendor_id: string;
domains: string;
};

export type DictDataUse = {
vendor_id: string;
vendor_name: string;
data_use: string;
data_categories: string[];
features: string[];
legal_basis_for_processing: string;
retention_period: number;
purpose: number;
special_purpose: number;
cookies: any[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
Checkbox,
Table,
Tag,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
} from "@fidesui/react";

import { DataUse } from "../../../types/api";
import { DictDataUse } from "../../plus/types";

interface Props {
onChange: (dataUses: DictDataUse[]) => void;
allDataUses: DataUse[];
dictDataUses: DictDataUse[];
checked: DictDataUse[];
}

const DataUseCheckboxTable = ({
onChange,
allDataUses,
dictDataUses,
checked,
}: Props) => {
const handleChangeAll = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
onChange(dictDataUses);
} else {
onChange([]);
}
};

const onCheck = (dataUse: DictDataUse) => {
const exists =
checked.filter((du) => du.data_use === dataUse.data_use).length > 0;
if (!exists) {
const newChecked = [...checked, dataUse];
onChange(newChecked);
} else {
const newChecked = checked.filter(
(use) => use.data_use !== dataUse.data_use
);
onChange(newChecked);
}
};

const declarationTitle = (declaration: DictDataUse) => {
const dataUse = allDataUses.filter(
(du) => du.fides_key === declaration.data_use
)[0];
if (dataUse) {
return dataUse.name;
}
return declaration.data_use;
};

const allChecked = dictDataUses.length === checked.length;

return (
<Table variant="unstyled" size="sm" border="1px" borderColor="gray.200">
<Thead border="1px" borderColor="gray.200" backgroundColor="gray.50">
<Tr>
<Th width={3} borderRight="1px" borderRightColor="gray.200">
<Checkbox
py={2}
colorScheme="complimentary"
isChecked={allChecked}
onChange={handleChangeAll}
data-testid="select-all"
/>
</Th>
<Th>
<Text
fontSize="md"
fontWeight="500"
casing="none"
letterSpacing={0}
>
Data use
</Text>
</Th>
</Tr>
</Thead>
<Tbody>
{dictDataUses.map((du) => (
<Tr key={du.data_use} border="1px" borderColor="gray.200">
<Td borderRight="1px" borderRightColor="gray.200">
<Checkbox
colorScheme="complimentary"
value={du.data_use}
isChecked={
checked.filter((use) => use.data_use === du.data_use).length >
0
}
onChange={() => onCheck(du)}
data-testid={`checkbox-${du.data_use}`}
/>
</Td>
<Td>
<Tag
size="lg"
py={1}
color="white"
backgroundColor="purple.500"
fontWeight="semibold"
>
{declarationTitle(du)}
</Tag>
</Td>
</Tr>
))}
</Tbody>
</Table>
);
};

export default DataUseCheckboxTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { AddIcon, WarningTwoIcon } from "@chakra-ui/icons";
import { Box, Button, HStack, Stack, Text, Tooltip } from "@fidesui/react";
import { ReactNode, useMemo } from "react";

import { SparkleIcon } from "../../common/Icon/SparkleIcon";

type Props = {
title: string;
description: string | ReactNode;
dictAvailable: boolean;
handleAdd: () => void;
handleDictSuggestion: () => void;
vendorSelected: boolean;
};

const EmptyTableState = ({
title,
description,
dictAvailable = false,
handleAdd,
handleDictSuggestion,
vendorSelected,
}: Props) => {
const dictDisabledTooltip = useMemo(
() =>
"You will need to select a vendor for this system before you can use the Fides dictionary. You can do this on System Information tab above.",
[]
);

return (
<Stack
backgroundColor="gray.50"
border="1px solid"
borderColor={dictAvailable ? "purple.400" : "blue.500"}
borderRadius="md"
justifyContent="space-between"
py={4}
px={6}
data-testid="empty-state"
>
<HStack>
{dictAvailable ? (
<SparkleIcon alignSelf="start" color="purple.400" mt={0.5} />
) : (
<WarningTwoIcon alignSelf="start" color="blue.400" mt={0.5} />
)}

<Box>
<Text fontWeight="bold" fontSize="sm" mb={1}>
{title}
</Text>

<Text fontSize="sm" color="gray.600" lineHeight="5">
{description}
</Text>
<HStack mt={4}>
{dictAvailable ? (
<>
<Tooltip
hasArrow
placement="top"
label={dictDisabledTooltip}
isDisabled={vendorSelected}
shouldWrapChildren
>
<Button
size="xs"
colorScheme="purple"
fontWeight="semibold"
data-testid="dict-btn"
onClick={handleDictSuggestion}
rightIcon={<SparkleIcon />}
disabled={!vendorSelected}
>
Generate data uses automatically
</Button>
</Tooltip>
<Text size="sm">or</Text>
</>
) : null}
<Button
size="xs"
colorScheme="black"
backgroundColor="primary.800"
fontWeight="semibold"
data-testid="add-btn"
onClick={handleAdd}
rightIcon={<AddIcon boxSize={2} />}
>
Add data use
</Button>
</HStack>
</Box>
</HStack>
</Stack>
);
};

export default EmptyTableState;
Loading

0 comments on commit 0e93f5a

Please sign in to comment.