Skip to content

Commit

Permalink
Privacy notice table scopes (#3007)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisonking authored Apr 10, 2023
1 parent dd3fc1b commit 8cd6474
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 87 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ The types of changes are:
- Added preliminary privacy notice page [#2995](https://github.com/ethyca/fides/pull/2995)
- Table for privacy notices [#3001](https://github.com/ethyca/fides/pull/3001)
- Query params on connection type endpoint to filter by supported action type [#2996](https://github.com/ethyca/fides/pull/2996)
- Scope restrictions for privacy notice table in the UI [#3007](https://github.com/ethyca/fides/pull/3007)
- Toggle for enabling/disabling privacy notices in the UI [#3010](https://github.com/ethyca/fides/pull/3010)
- Add endpoint to retrieve privacy notices grouped by their associated data uses [#2956](https://github.com/ethyca/fides/pull/2956)
- Support for uploading custom connector templates via the UI [#2997](https://github.com/ethyca/fides/pull/2997)

Expand Down
91 changes: 89 additions & 2 deletions clients/admin-ui/cypress/e2e/privacy-notices.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { stubPlus } from "cypress/support/stubs";
import { PRIVACY_NOTICES_ROUTE } from "~/features/common/nav/v2/routes";
import { RoleRegistryEnum } from "~/types/api";

const DATA_SALES_NOTICE_ID = "pri_afd25287-cce4-487a-a6b4-b7647b068990";
const ESSENTIAL_NOTICE_ID = "pri_e76cbe20-6ffa-46b4-9a91-b1ae3412dd49";

describe("Privacy notices", () => {
beforeEach(() => {
cy.login();
cy.intercept("GET", "/api/v1/privacy-notice/*", {
cy.intercept("GET", "/api/v1/privacy-notice*", {
fixture: "privacy-notices/list.json",
}).as("getNotices");
stubPlus(true);
Expand All @@ -31,11 +34,53 @@ describe("Privacy notices", () => {
cy.getByTestId("privacy-notices-page");
});
});

it("viewers and approvers cannot click into a notice to edit", () => {
[RoleRegistryEnum.VIEWER, RoleRegistryEnum.VIEWER_AND_APPROVER].forEach(
(role) => {
cy.assumeRole(role);
cy.visit(PRIVACY_NOTICES_ROUTE);
cy.wait("@getNotices");
cy.getByTestId("row-Essential").click();
// we should still be on the same page
cy.getByTestId("privacy-notice-detail-page").should("not.exist");
cy.getByTestId("privacy-notices-page");
}
);
});

it("viewers and approvers cannot toggle the enable toggle", () => {
[RoleRegistryEnum.VIEWER, RoleRegistryEnum.VIEWER_AND_APPROVER].forEach(
(role) => {
cy.assumeRole(role);
cy.visit(PRIVACY_NOTICES_ROUTE);
cy.wait("@getNotices");
cy.getByTestId("toggle-Enable")
.first()
.within(() => {
cy.get("span").should("have.attr", "data-disabled");
});
}
);
});

it("viewers and approvers cannot add notices", () => {
[RoleRegistryEnum.VIEWER, RoleRegistryEnum.VIEWER_AND_APPROVER].forEach(
(role) => {
cy.assumeRole(role);
cy.visit(PRIVACY_NOTICES_ROUTE);
cy.wait("@getNotices");
cy.getByTestId("privacy-notices-page");
cy.getByTestId("add-privacy-notice-btn").should("not.exist");
}
);
});
});

describe("table", () => {
beforeEach(() => {
cy.visit(PRIVACY_NOTICES_ROUTE);
cy.wait("@getNotices");
});

it("should render a row for each privacy notice", () => {
Expand Down Expand Up @@ -64,7 +109,49 @@ describe("Privacy notices", () => {
it("can click a row to go to the notice page", () => {
cy.getByTestId("row-Essential").click();
cy.getByTestId("privacy-notice-detail-page");
cy.url().should("contain", "pri_e76cbe20-6ffa-46b4-9a91-b1ae3412dd49");
cy.url().should("contain", ESSENTIAL_NOTICE_ID);
});

describe("enabling and disabling", () => {
beforeEach(() => {
cy.intercept("PATCH", "/api/v1/privacy-notice*", {
fixture: "privacy-notices/list.json",
}).as("patchNotices");
});

it("can enable a notice", () => {
cy.getByTestId("row-Data Sales").within(() => {
cy.getByTestId("toggle-Enable").within(() => {
cy.get("span").should("not.have.attr", "data-checked");
});
cy.getByTestId("toggle-Enable").click();
});

cy.wait("@patchNotices").then((interception) => {
const { body } = interception.request;
expect(body).to.eql([{ id: DATA_SALES_NOTICE_ID, disabled: false }]);
});
// redux should requery after invalidation
cy.wait("@getNotices");
});

it("can disable a notice with a warning", () => {
cy.getByTestId("row-Essential").within(() => {
cy.getByTestId("toggle-Enable").within(() => {
cy.get("span").should("have.attr", "data-checked");
});
cy.getByTestId("toggle-Enable").click();
});

cy.getByTestId("confirmation-modal");
cy.getByTestId("continue-btn").click();
cy.wait("@patchNotices").then((interception) => {
const { body } = interception.request;
expect(body).to.eql([{ id: ESSENTIAL_NOTICE_ID, disabled: true }]);
});
// redux should requery after invalidation
cy.wait("@getNotices");
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"consent_mechanism": "opt_out",
"data_uses": ["advertising.third_party.personalized"],
"enforcement_level": "frontend",
"disabled": false,
"disabled": true,
"has_gpc_flag": false,
"displayed_in_privacy_center": true,
"displayed_in_privacy_modal": true,
Expand Down
12 changes: 10 additions & 2 deletions clients/admin-ui/src/features/common/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { ThemingProps } from "@chakra-ui/system";
import {
Button,
Center,
Modal,
ModalBody,
ModalContent,
Expand All @@ -26,6 +27,7 @@ interface Props {
returnFocusOnClose?: boolean;
isCentered?: boolean;
testId?: string;
icon?: ReactNode;
}
const ConfirmationModal = ({
isOpen,
Expand All @@ -41,6 +43,7 @@ const ConfirmationModal = ({
returnFocusOnClose,
isCentered,
testId = "confirmation-modal",
icon,
}: Props) => (
<Modal
isOpen={isOpen}
Expand All @@ -50,8 +53,13 @@ const ConfirmationModal = ({
isCentered={isCentered}
>
<ModalOverlay />
<ModalContent textAlign="center" p={2} data-testid={testId}>
{title ? <ModalHeader fontWeight="medium">{title}</ModalHeader> : null}
<ModalContent textAlign="center" p={6} data-testid={testId}>
{icon ? <Center mb={2}>{icon}</Center> : null}
{title ? (
<ModalHeader fontWeight="medium" pb={0}>
{title}
</ModalHeader>
) : null}
{message ? <ModalBody>{message}</ModalBody> : null}
<ModalFooter>
<SimpleGrid columns={2} width="100%">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ import {
Tr,
} from "@fidesui/react";
import { useRouter } from "next/router";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { ReactNode, useMemo } from "react";
import { Column, useSortBy, useTable } from "react-table";

import { useAppSelector } from "~/app/hooks";
import { PrivacyNoticeResponse } from "~/types/api";
import { PRIVACY_NOTICES_ROUTE } from "~/features/common/nav/v2/routes";
import Restrict, { useHasPermission } from "~/features/common/Restrict";
import { PrivacyNoticeResponse, ScopeRegistryEnum } from "~/types/api";

import { PRIVACY_NOTICES_ROUTE } from "../common/nav/v2/routes";
import {
DateCell,
EnablePrivacyNoticeCell,
MechanismCell,
MultiTagCell,
TitleCell,
ToggleCell,
WrappedCell,
} from "./cells";
import {
Expand All @@ -35,14 +36,6 @@ import {
useGetAllPrivacyNoticesQuery,
} from "./privacy-notices.slice";

/**
* We add an 'enabled' flag so that the UI can toggle the 'enabled' state
* without directly modifying the PrivacyNoticeResponse data
*/
interface PrivacyNoticeTableData extends PrivacyNoticeResponse {
enabled: boolean;
}

const PrivacyNoticesTable = () => {
const router = useRouter();
// Subscribe to get all privacy notices
Expand All @@ -52,34 +45,12 @@ const PrivacyNoticesTable = () => {

const privacyNotices = useAppSelector(selectAllPrivacyNotices);

// Sync up our enabled state with the initial privacy notice enabled state
const [enabledNoticeIds, setEnabledNoticeIds] = useState<string[]>([]);
useEffect(() => {
setEnabledNoticeIds(
privacyNotices.filter((pn) => !pn.disabled).map((pn) => pn.id)
);
}, [privacyNotices]);

const handleToggle = useCallback(
(privacyNoticeId: PrivacyNoticeTableData["id"]) => {
const isEnabled = !!enabledNoticeIds.find(
(enabledId) => enabledId === privacyNoticeId
);

if (isEnabled) {
setEnabledNoticeIds(
enabledNoticeIds.filter(
(enabledNoticeId) => enabledNoticeId !== privacyNoticeId
)
);
} else {
setEnabledNoticeIds([...enabledNoticeIds, privacyNoticeId]);
}
},
[enabledNoticeIds]
);
// Permissions
const userCanUpdate = useHasPermission([
ScopeRegistryEnum.PRIVACY_NOTICE_UPDATE,
]);

const columns: Column<PrivacyNoticeTableData>[] = useMemo(
const columns: Column<PrivacyNoticeResponse>[] = useMemo(
() => [
{
Header: "Title",
Expand All @@ -101,27 +72,15 @@ const PrivacyNoticesTable = () => {
{ Header: "Last update", accessor: "updated_at", Cell: DateCell },
{
Header: "Enable",
accessor: "enabled",
onToggle: handleToggle,
Cell: ToggleCell,
accessor: "disabled",
disabled: !userCanUpdate,
Cell: EnablePrivacyNoticeCell,
},
],
[handleToggle]
);

// Create the data object as the PrivacyNoticeResponse + a UI only "enabled" field
// which the UI can control the state of
const data: PrivacyNoticeTableData[] = useMemo(
() =>
privacyNotices.map((pn) => {
const isEnabled =
enabledNoticeIds.filter((noticeId) => noticeId === pn.id).length > 0;
return { ...pn, enabled: isEnabled };
}),
[privacyNotices, enabledNoticeIds]
[userCanUpdate]
);

const tableInstance = useTable({ columns, data }, useSortBy);
const tableInstance = useTable({ columns, data: privacyNotices }, useSortBy);

const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
tableInstance;
Expand Down Expand Up @@ -187,13 +146,19 @@ const PrivacyNoticesTable = () => {
prepareRow(row);
const { key: rowKey, ...rowProps } = row.getRowProps();
const onClick = () => {
router.push(`${PRIVACY_NOTICES_ROUTE}/${row.original.id}`);
if (userCanUpdate) {
router.push(`${PRIVACY_NOTICES_ROUTE}/${row.original.id}`);
}
};
return (
<Tr
key={rowKey}
{...rowProps}
_hover={{ backgroundColor: "gray.50", cursor: "pointer" }}
_hover={
userCanUpdate
? { backgroundColor: "gray.50", cursor: "pointer" }
: undefined
}
data-testid={`row-${row.original.name}`}
>
{row.cells.map((cell) => {
Expand All @@ -219,9 +184,15 @@ const PrivacyNoticesTable = () => {
<Tfoot backgroundColor="gray.50">
<Tr>
<Td colSpan={columns.length} px={4} py={3.5}>
<Button size="xs" colorScheme="primary">
Add a privacy notice +
</Button>
<Restrict scopes={[ScopeRegistryEnum.PRIVACY_NOTICE_CREATE]}>
<Button
size="xs"
colorScheme="primary"
data-testid="add-privacy-notice-btn"
>
Add a privacy notice +
</Button>
</Restrict>
</Td>
</Tr>
</Tfoot>
Expand Down
Loading

0 comments on commit 8cd6474

Please sign in to comment.