diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4f3e1b20..94cd65471b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The types of changes are: - Access and erasure support for Amplitude [#2569](https://github.com/ethyca/fides/pull/2569) - Access and erasure support for Gorgias [#2444](https://github.com/ethyca/fides/pull/2444) - Privacy Experience Bulk Create, Bulk Update, and Detail Endpoints [#3185](https://github.com/ethyca/fides/pull/3185) +- Initial privacy experience UI [#3186](https://github.com/ethyca/fides/pull/3186) ### Changed diff --git a/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts b/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts new file mode 100644 index 0000000000..22a6402771 --- /dev/null +++ b/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts @@ -0,0 +1,147 @@ +import { stubPlus } from "cypress/support/stubs"; + +import { PRIVACY_EXPERIENCE_ROUTE } from "~/features/common/nav/v2/routes"; +import { RoleRegistryEnum } from "~/types/api"; + +const OVERLAY_EXPERIENCE_ID = "pri_4076d6dd-a728-4f2d-9a5c-98f35d7a5a86"; +const DISABLED_EXPERIENCE_ID = "pri_75d2b3dc-6f7c-4a36-95ee-3088bd1b4572"; + +describe("Privacy experiences", () => { + beforeEach(() => { + cy.login(); + cy.intercept("GET", "/api/v1/privacy-experience*", { + fixture: "privacy-experiences/list.json", + }).as("getExperiences"); + stubPlus(true); + }); + + describe("permissions", () => { + it("should not be viewable for approvers", () => { + cy.assumeRole(RoleRegistryEnum.APPROVER); + cy.visit(PRIVACY_EXPERIENCE_ROUTE); + // should be redirected to the home page + cy.getByTestId("home-content"); + }); + + it("should be visible to everyone else", () => { + [ + RoleRegistryEnum.CONTRIBUTOR, + RoleRegistryEnum.OWNER, + RoleRegistryEnum.VIEWER, + ].forEach((role) => { + cy.assumeRole(role); + cy.visit(PRIVACY_EXPERIENCE_ROUTE); + cy.getByTestId("privacy-experience-page"); + }); + }); + + it("viewers and approvers cannot click into an experience to edit", () => { + [RoleRegistryEnum.VIEWER, RoleRegistryEnum.VIEWER_AND_APPROVER].forEach( + (role) => { + cy.assumeRole(role); + cy.visit(PRIVACY_EXPERIENCE_ROUTE); + cy.wait("@getExperiences"); + cy.getByTestId(`row-${OVERLAY_EXPERIENCE_ID}`).click(); + // we should still be on the same page + cy.getByTestId("privacy-experience-detail-page").should("not.exist"); + cy.getByTestId("privacy-experience-page"); + } + ); + }); + + it("viewers and approvers cannot toggle the enable toggle", () => { + [RoleRegistryEnum.VIEWER, RoleRegistryEnum.VIEWER_AND_APPROVER].forEach( + (role) => { + cy.assumeRole(role); + cy.visit(PRIVACY_EXPERIENCE_ROUTE); + cy.wait("@getExperiences"); + cy.getByTestId("toggle-Enable") + .first() + .within(() => { + cy.get("span").should("have.attr", "data-disabled"); + }); + } + ); + }); + }); + + it("can show an empty state", () => { + cy.intercept("GET", "/api/v1/privacy-experience*", { + body: { items: [], page: 1, size: 10, total: 0 }, + }).as("getEmptyExperiences"); + cy.visit(PRIVACY_EXPERIENCE_ROUTE); + cy.wait("@getEmptyExperiences"); + cy.getByTestId("empty-state"); + }); + + describe("table", () => { + beforeEach(() => { + cy.visit(PRIVACY_EXPERIENCE_ROUTE); + cy.wait("@getExperiences"); + }); + + it("should render a row for each privacy experience", () => { + cy.fixture("privacy-experiences/list.json").then((data) => { + data.items + .map((item) => item.id) + .forEach((id) => { + cy.getByTestId(`row-${id}`); + }); + }); + }); + + it("can click a row to go to the experience page", () => { + cy.intercept("GET", "/api/v1/privacy-experience/pri*", { + fixture: "privacy-experiences/experience.json", + }).as("getExperienceDetail"); + cy.getByTestId(`row-${OVERLAY_EXPERIENCE_ID}`).click(); + cy.wait("@getExperienceDetail"); + cy.getByTestId("privacy-experience-detail-page"); + cy.url().should("contain", OVERLAY_EXPERIENCE_ID); + }); + + describe("enabling and disabling", () => { + beforeEach(() => { + cy.intercept("PATCH", "/api/v1/privacy-experience*", { + fixture: "privacy-experiences/list.json", + }).as("patchExperiences"); + }); + + it("can enable an experience", () => { + cy.getByTestId(`row-${DISABLED_EXPERIENCE_ID}`).within(() => { + cy.getByTestId("toggle-Enable").within(() => { + cy.get("span").should("not.have.attr", "data-checked"); + }); + cy.getByTestId("toggle-Enable").click(); + }); + + cy.wait("@patchExperiences").then((interception) => { + const { body } = interception.request; + expect(body).to.eql([ + { id: DISABLED_EXPERIENCE_ID, disabled: false }, + ]); + }); + // redux should requery after invalidation + cy.wait("@getExperiences"); + }); + + it("can disable an experience with a warning", () => { + cy.getByTestId(`row-${OVERLAY_EXPERIENCE_ID}`).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("@patchExperiences").then((interception) => { + const { body } = interception.request; + expect(body).to.eql([{ id: OVERLAY_EXPERIENCE_ID, disabled: true }]); + }); + // redux should requery after invalidation + cy.wait("@getExperiences"); + }); + }); + }); +}); diff --git a/clients/admin-ui/cypress/fixtures/privacy-experiences/experience.json b/clients/admin-ui/cypress/fixtures/privacy-experiences/experience.json new file mode 100644 index 0000000000..c950edbe7a --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/privacy-experiences/experience.json @@ -0,0 +1,21 @@ +{ + "disabled": false, + "component": "overlay", + "delivery_mechanism": "link", + "regions": ["eu_de"], + "component_title": null, + "component_description": null, + "banner_title": null, + "banner_description": null, + "link_label": null, + "confirmation_button_label": null, + "reject_button_label": null, + "acknowledgement_button_label": null, + "id": "pri_4076d6dd-a728-4f2d-9a5c-98f35d7a5a86", + "created_at": "2023-05-02T15:32:41.253621+00:00", + "updated_at": "2023-05-02T15:32:41.253621+00:00", + "version": 1.0, + "privacy_experience_history_id": "pri_fa40ddc5-ba8d-4efe-b83f-643b2595b191", + "privacy_experience_template_id": null, + "privacy_notices": [] +} diff --git a/clients/admin-ui/cypress/fixtures/privacy-experiences/list.json b/clients/admin-ui/cypress/fixtures/privacy-experiences/list.json new file mode 100644 index 0000000000..5967d09700 --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/privacy-experiences/list.json @@ -0,0 +1,172 @@ +{ + "items": [ + { + "disabled": false, + "component": "overlay", + "delivery_mechanism": "link", + "regions": ["eu_de"], + "component_title": null, + "component_description": null, + "banner_title": null, + "banner_description": null, + "link_label": null, + "confirmation_button_label": null, + "reject_button_label": null, + "acknowledgement_button_label": null, + "id": "pri_4076d6dd-a728-4f2d-9a5c-98f35d7a5a86", + "created_at": "2023-05-02T15:32:41.253621+00:00", + "updated_at": "2023-05-02T15:32:41.253621+00:00", + "version": 1.0, + "privacy_experience_history_id": "pri_fa40ddc5-ba8d-4efe-b83f-643b2595b191", + "privacy_experience_template_id": null, + "privacy_notices": [] + }, + { + "disabled": true, + "component": "privacy_center", + "delivery_mechanism": "link", + "regions": ["us_ca"], + "component_title": null, + "component_description": null, + "banner_title": null, + "banner_description": null, + "link_label": null, + "confirmation_button_label": null, + "reject_button_label": null, + "acknowledgement_button_label": null, + "id": "pri_75d2b3dc-6f7c-4a36-95ee-3088bd1b4572", + "created_at": "2023-05-02T15:32:41.237150+00:00", + "updated_at": "2023-05-02T15:32:41.237150+00:00", + "version": 1.0, + "privacy_experience_history_id": "pri_422201ba-59c6-48df-992a-f20af701e999", + "privacy_experience_template_id": null, + "privacy_notices": [ + { + "name": "Data Sales", + "description": "Provide opt-out consent for the use of data in ways that may be considered “data sales” under US state privacy regulations.", + "internal_description": null, + "origin": null, + "regions": ["us_ca", "us_co"], + "consent_mechanism": "opt_out", + "data_uses": ["advertising.third_party.personalized"], + "enforcement_level": "frontend", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": true, + "displayed_in_overlay": false, + "displayed_in_api": false, + "id": "pri_8ad94105-6142-4f20-bb56-9644a6f16917", + "created_at": "2023-05-02T15:19:36.733208+00:00", + "updated_at": "2023-05-02T15:19:36.733208+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_b524bbec-22e2-4364-9e39-5399208976d8" + } + ] + }, + { + "disabled": false, + "component": "overlay", + "delivery_mechanism": "banner", + "regions": ["eu_fr", "eu_ie"], + "component_title": null, + "component_description": null, + "banner_title": null, + "banner_description": null, + "link_label": null, + "confirmation_button_label": null, + "reject_button_label": null, + "acknowledgement_button_label": null, + "id": "pri_759df3b9-57c7-40aa-8252-0a12456d81cb", + "created_at": "2023-05-02T15:32:41.219842+00:00", + "updated_at": "2023-05-02T15:32:41.219842+00:00", + "version": 1.0, + "privacy_experience_history_id": "pri_438e7d81-0ad0-43e5-8c1e-2c45fe8d6177", + "privacy_experience_template_id": null, + "privacy_notices": [ + { + "name": "Advertising", + "description": "Ensures you are correctly notifying the user about your advertising practices and for appropriate locations, collecting the users consent preferences.", + "internal_description": null, + "origin": null, + "regions": ["eu_fr", "eu_ie"], + "consent_mechanism": "opt_in", + "data_uses": ["advertising.first_party.contextual"], + "enforcement_level": "system_wide", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": false, + "displayed_in_overlay": true, + "displayed_in_api": false, + "id": "pri_82efe831-b134-434f-90eb-e4b3204a5e97", + "created_at": "2023-05-02T15:19:36.730578+00:00", + "updated_at": "2023-05-02T15:19:36.730578+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_f19c3323-7281-4043-b964-a103835cab4b" + }, + { + "name": "Analytics", + "description": "Ensures you are correctly notifying the user about your advertising practices and for appropriate locations, collecting the users consent preferences.", + "internal_description": null, + "origin": null, + "regions": ["eu_fr", "eu_ie"], + "consent_mechanism": "opt_in", + "data_uses": ["collect"], + "enforcement_level": "system_wide", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": false, + "displayed_in_overlay": true, + "displayed_in_api": false, + "id": "pri_1e98649f-9294-4217-a51a-2d2781f19052", + "created_at": "2023-05-02T15:19:36.727792+00:00", + "updated_at": "2023-05-02T15:19:36.727792+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_18e70ccb-af22-4761-bf77-4095ee19118f" + }, + { + "name": "Functional", + "description": "This is for data processing activities that enhance the capability or features of your site but may not be strictly necessary.", + "internal_description": null, + "origin": null, + "regions": ["eu_fr", "eu_ie"], + "consent_mechanism": "opt_in", + "data_uses": ["improve.system"], + "enforcement_level": "system_wide", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": false, + "displayed_in_overlay": true, + "displayed_in_api": false, + "id": "pri_97f45baf-298e-4ea0-a22e-cf80543b573b", + "created_at": "2023-05-02T15:19:36.723747+00:00", + "updated_at": "2023-05-02T15:19:36.723747+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_595eef41-9848-4709-be5c-cf924d6e5968" + }, + { + "name": "Essential", + "description": "Notify the user about data processing activities that are essential to your services functionality. Typically consent is not required for this.", + "internal_description": null, + "origin": null, + "regions": ["eu_fr", "eu_ie"], + "consent_mechanism": "notice_only", + "data_uses": ["provide.service"], + "enforcement_level": "system_wide", + "disabled": false, + "has_gpc_flag": false, + "displayed_in_privacy_center": false, + "displayed_in_overlay": true, + "displayed_in_api": false, + "id": "pri_e8d8c006-5753-4b13-afc6-4f26caf6eb61", + "created_at": "2023-05-02T15:19:36.703598+00:00", + "updated_at": "2023-05-02T15:19:36.703598+00:00", + "version": 1.0, + "privacy_notice_history_id": "pri_79eba2d7-d6f2-4bd4-94b9-f02dbc2f7621" + } + ] + } + ], + "total": 3, + "page": 1, + "size": 10 +} diff --git a/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json b/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json index aac1cf2ed9..fe99fd21e2 100644 --- a/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json +++ b/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json @@ -1,353 +1,365 @@ { - "owner": [ - "allow_list:create", - "allow_list:delete", - "allow_list:read", - "allow_list:update", - "classify_instance:create", - "classify_instance:read", - "classify_instance:update", - "cli-objects:create", - "cli-objects:delete", - "cli-objects:read", - "cli-objects:update", - "client:create", - "client:delete", - "client:read", - "client:update", - "config:read", - "config:update", - "connection:authorize", - "connection:create_or_update", - "connection:delete", - "connection:instantiate", - "connection:read", - "connection_type:read", - "connector_template:register", - "consent:read", - "ctl_dataset:create", - "ctl_dataset:delete", - "ctl_dataset:read", - "ctl_dataset:update", - "ctl_policy:create", - "ctl_policy:delete", - "ctl_policy:read", - "ctl_policy:update", - "custom_field:create", - "custom_field:delete", - "custom_field:read", - "custom_field:update", - "custom_field_definition:create", - "custom_field_definition:delete", - "custom_field_definition:read", - "custom_field_definition:update", - "data_category:create", - "data_category:delete", - "data_category:read", - "data_category:update", - "data_qualifier:create", - "data_qualifier:delete", - "data_qualifier:read", - "data_qualifier:update", - "data_subject:create", - "data_subject:delete", - "data_subject:read", - "data_subject:update", - "data_use:create", - "data_use:delete", - "data_use:read", - "data_use:update", - "database:reset", - "datamap:read", - "dataset:create_or_update", - "dataset:delete", - "dataset:read", - "encryption:exec", - "evaluation:create", - "evaluation:delete", - "evaluation:read", - "evaluation:update", - "fides_taxonomy:update", - "generate:exec", - "masking:exec", - "masking:read", - "messaging:create_or_update", - "messaging:delete", - "messaging:read", - "organization:create", - "organization:delete", - "organization:read", - "organization:update", - "policy:create_or_update", - "policy:delete", - "policy:read", - "privacy-notice:create", - "privacy-notice:read", - "privacy-notice:update", - "privacy-request-notifications:create_or_update", - "privacy-request-notifications:read", - "privacy-request:create", - "privacy-request:delete", - "privacy-request:read", - "privacy-request:resume", - "privacy-request:review", - "privacy-request:transfer", - "privacy-request:upload_data", - "privacy-request:view_data", - "registry:create", - "registry:delete", - "registry:read", - "registry:update", - "rule:create_or_update", - "rule:delete", - "rule:read", - "saas_config:create_or_update", - "saas_config:delete", - "saas_config:read", - "scope:read", - "storage:create_or_update", - "storage:delete", - "storage:read", - "system:create", - "system:delete", - "system:read", - "system:update", - "system_manager:delete", - "system_manager:read", - "system_manager:update", - "system_scan:create", - "system_scan:read", - "taxonomy:create", - "taxonomy:delete", - "taxonomy:update", - "user-permission:assign_owners", - "user-permission:create", - "user-permission:read", - "user-permission:update", - "user:create", - "user:delete", - "user:password-reset", - "user:read", - "user:update", - "validate:exec", - "webhook:create_or_update", - "webhook:delete", - "webhook:read" - ], - "viewer_and_approver": [ - "allow_list:read", - "classify_instance:read", - "cli-objects:read", - "client:read", - "config:read", - "connection:read", - "connection_type:read", - "consent:read", - "ctl_dataset:read", - "ctl_policy:read", - "custom_field:read", - "custom_field_definition:read", - "data_category:read", - "data_qualifier:read", - "data_subject:read", - "data_use:read", - "datamap:read", - "dataset:read", - "evaluation:read", - "masking:exec", - "masking:read", - "messaging:read", - "organization:read", - "policy:read", - "privacy-notice:read", - "privacy-request-notifications:read", - "privacy-request:read", - "privacy-request:resume", - "privacy-request:review", - "privacy-request:upload_data", - "privacy-request:view_data", - "registry:read", - "rule:read", - "saas_config:read", - "scope:read", - "storage:read", - "system:read", - "system_manager:read", - "system_scan:read", - "user:read", - "webhook:read" - ], - "viewer": [ - "allow_list:read", - "classify_instance:read", - "cli-objects:read", - "client:read", - "config:read", - "connection:read", - "connection_type:read", - "consent:read", - "ctl_dataset:read", - "ctl_policy:read", - "custom_field:read", - "custom_field_definition:read", - "data_category:read", - "data_qualifier:read", - "data_subject:read", - "data_use:read", - "datamap:read", - "dataset:read", - "evaluation:read", - "masking:exec", - "masking:read", - "messaging:read", - "organization:read", - "policy:read", - "privacy-notice:read", - "privacy-request-notifications:read", - "privacy-request:read", - "registry:read", - "rule:read", - "saas_config:read", - "scope:read", - "storage:read", - "system:read", - "system_manager:read", - "system_scan:read", - "user:read", - "webhook:read" - ], - "approver": [ - "privacy-request:read", - "privacy-request:resume", - "privacy-request:review", - "privacy-request:upload_data", - "privacy-request:view_data" - ], - "contributor": [ - "allow_list:create", - "allow_list:delete", - "allow_list:read", - "allow_list:update", - "classify_instance:create", - "classify_instance:read", - "classify_instance:update", - "cli-objects:create", - "cli-objects:delete", - "cli-objects:read", - "cli-objects:update", - "client:create", - "client:delete", - "client:read", - "client:update", - "config:read", - "connection:authorize", - "connection:create_or_update", - "connection:delete", - "connection:instantiate", - "connection:read", - "connection_type:read", - "consent:read", - "ctl_dataset:create", - "ctl_dataset:delete", - "ctl_dataset:read", - "ctl_dataset:update", - "ctl_policy:create", - "ctl_policy:delete", - "ctl_policy:read", - "ctl_policy:update", - "custom_field:create", - "custom_field:delete", - "custom_field:read", - "custom_field:update", - "custom_field_definition:create", - "custom_field_definition:delete", - "custom_field_definition:read", - "custom_field_definition:update", - "data_category:create", - "data_category:delete", - "data_category:read", - "data_category:update", - "data_qualifier:create", - "data_qualifier:delete", - "data_qualifier:read", - "data_qualifier:update", - "data_subject:create", - "data_subject:delete", - "data_subject:read", - "data_subject:update", - "data_use:create", - "data_use:delete", - "data_use:read", - "data_use:update", - "database:reset", - "datamap:read", - "dataset:create_or_update", - "dataset:delete", - "dataset:read", - "encryption:exec", - "evaluation:create", - "evaluation:delete", - "evaluation:read", - "evaluation:update", - "fides_taxonomy:update", - "generate:exec", - "masking:exec", - "masking:read", - "messaging:read", - "organization:create", - "organization:delete", - "organization:read", - "organization:update", - "policy:create_or_update", - "policy:delete", - "policy:read", - "privacy-notice:create", - "privacy-notice:read", - "privacy-notice:update", - "privacy-request-notifications:read", - "privacy-request:create", - "privacy-request:delete", - "privacy-request:read", - "privacy-request:resume", - "privacy-request:review", - "privacy-request:transfer", - "privacy-request:upload_data", - "privacy-request:view_data", - "registry:create", - "registry:delete", - "registry:read", - "registry:update", - "rule:create_or_update", - "rule:delete", - "rule:read", - "saas_config:create_or_update", - "saas_config:delete", - "saas_config:read", - "scope:read", - "storage:read", - "system:create", - "system:delete", - "system:read", - "system:update", - "system_manager:delete", - "system_manager:read", - "system_manager:update", - "system_scan:create", - "system_scan:read", - "taxonomy:create", - "taxonomy:delete", - "taxonomy:update", - "user-permission:create", - "user-permission:read", - "user-permission:update", - "user:create", - "user:delete", - "user:password-reset", - "user:read", - "user:update", - "validate:exec", - "webhook:create_or_update", - "webhook:delete", - "webhook:read" - ] -} \ No newline at end of file + "owner": [ + "allow_list:create", + "allow_list:delete", + "allow_list:read", + "allow_list:update", + "classify_instance:create", + "classify_instance:read", + "classify_instance:update", + "cli-objects:create", + "cli-objects:delete", + "cli-objects:read", + "cli-objects:update", + "client:create", + "client:delete", + "client:read", + "client:update", + "config:read", + "config:update", + "connection:authorize", + "connection:create_or_update", + "connection:delete", + "connection:instantiate", + "connection:read", + "connection_type:read", + "connector_template:register", + "consent:read", + "ctl_dataset:create", + "ctl_dataset:delete", + "ctl_dataset:read", + "ctl_dataset:update", + "ctl_policy:create", + "ctl_policy:delete", + "ctl_policy:read", + "ctl_policy:update", + "current-privacy-preference:read", + "custom_field:create", + "custom_field:delete", + "custom_field:read", + "custom_field:update", + "custom_field_definition:create", + "custom_field_definition:delete", + "custom_field_definition:read", + "custom_field_definition:update", + "data_category:create", + "data_category:delete", + "data_category:read", + "data_category:update", + "data_qualifier:create", + "data_qualifier:delete", + "data_qualifier:read", + "data_qualifier:update", + "data_subject:create", + "data_subject:delete", + "data_subject:read", + "data_subject:update", + "data_use:create", + "data_use:delete", + "data_use:read", + "data_use:update", + "database:reset", + "datamap:read", + "dataset:create_or_update", + "dataset:delete", + "dataset:read", + "encryption:exec", + "evaluation:create", + "evaluation:delete", + "evaluation:read", + "evaluation:update", + "fides_taxonomy:update", + "generate:exec", + "masking:exec", + "masking:read", + "messaging:create_or_update", + "messaging:delete", + "messaging:read", + "organization:create", + "organization:delete", + "organization:read", + "organization:update", + "policy:create_or_update", + "policy:delete", + "policy:read", + "privacy-experience:create", + "privacy-experience:read", + "privacy-experience:update", + "privacy-notice:create", + "privacy-notice:read", + "privacy-notice:update", + "privacy-preference-history:read", + "privacy-request-notifications:create_or_update", + "privacy-request-notifications:read", + "privacy-request:create", + "privacy-request:delete", + "privacy-request:read", + "privacy-request:resume", + "privacy-request:review", + "privacy-request:transfer", + "privacy-request:upload_data", + "privacy-request:view_data", + "registry:create", + "registry:delete", + "registry:read", + "registry:update", + "rule:create_or_update", + "rule:delete", + "rule:read", + "saas_config:create_or_update", + "saas_config:delete", + "saas_config:read", + "scope:read", + "storage:create_or_update", + "storage:delete", + "storage:read", + "system:create", + "system:delete", + "system:read", + "system:update", + "system_manager:delete", + "system_manager:read", + "system_manager:update", + "system_scan:create", + "system_scan:read", + "taxonomy:create", + "taxonomy:delete", + "taxonomy:update", + "user-permission:assign_owners", + "user-permission:create", + "user-permission:read", + "user-permission:update", + "user:create", + "user:delete", + "user:password-reset", + "user:read", + "user:update", + "validate:exec", + "webhook:create_or_update", + "webhook:delete", + "webhook:read" + ], + "viewer_and_approver": [ + "allow_list:read", + "classify_instance:read", + "cli-objects:read", + "client:read", + "config:read", + "connection:read", + "connection_type:read", + "consent:read", + "ctl_dataset:read", + "ctl_policy:read", + "custom_field:read", + "custom_field_definition:read", + "data_category:read", + "data_qualifier:read", + "data_subject:read", + "data_use:read", + "datamap:read", + "dataset:read", + "evaluation:read", + "masking:exec", + "masking:read", + "messaging:read", + "organization:read", + "policy:read", + "privacy-experience:read", + "privacy-notice:read", + "privacy-request-notifications:read", + "privacy-request:read", + "privacy-request:resume", + "privacy-request:review", + "privacy-request:upload_data", + "privacy-request:view_data", + "registry:read", + "rule:read", + "saas_config:read", + "scope:read", + "storage:read", + "system:read", + "system_manager:read", + "system_scan:read", + "user:read", + "webhook:read" + ], + "viewer": [ + "allow_list:read", + "classify_instance:read", + "cli-objects:read", + "client:read", + "config:read", + "connection:read", + "connection_type:read", + "consent:read", + "ctl_dataset:read", + "ctl_policy:read", + "custom_field:read", + "custom_field_definition:read", + "data_category:read", + "data_qualifier:read", + "data_subject:read", + "data_use:read", + "datamap:read", + "dataset:read", + "evaluation:read", + "masking:exec", + "masking:read", + "messaging:read", + "organization:read", + "policy:read", + "privacy-experience:read", + "privacy-notice:read", + "privacy-request-notifications:read", + "privacy-request:read", + "registry:read", + "rule:read", + "saas_config:read", + "scope:read", + "storage:read", + "system:read", + "system_manager:read", + "system_scan:read", + "user:read", + "webhook:read" + ], + "approver": [ + "privacy-request:read", + "privacy-request:resume", + "privacy-request:review", + "privacy-request:upload_data", + "privacy-request:view_data" + ], + "contributor": [ + "allow_list:create", + "allow_list:delete", + "allow_list:read", + "allow_list:update", + "classify_instance:create", + "classify_instance:read", + "classify_instance:update", + "cli-objects:create", + "cli-objects:delete", + "cli-objects:read", + "cli-objects:update", + "client:create", + "client:delete", + "client:read", + "client:update", + "config:read", + "connection:authorize", + "connection:create_or_update", + "connection:delete", + "connection:instantiate", + "connection:read", + "connection_type:read", + "consent:read", + "ctl_dataset:create", + "ctl_dataset:delete", + "ctl_dataset:read", + "ctl_dataset:update", + "ctl_policy:create", + "ctl_policy:delete", + "ctl_policy:read", + "ctl_policy:update", + "current-privacy-preference:read", + "custom_field:create", + "custom_field:delete", + "custom_field:read", + "custom_field:update", + "custom_field_definition:create", + "custom_field_definition:delete", + "custom_field_definition:read", + "custom_field_definition:update", + "data_category:create", + "data_category:delete", + "data_category:read", + "data_category:update", + "data_qualifier:create", + "data_qualifier:delete", + "data_qualifier:read", + "data_qualifier:update", + "data_subject:create", + "data_subject:delete", + "data_subject:read", + "data_subject:update", + "data_use:create", + "data_use:delete", + "data_use:read", + "data_use:update", + "database:reset", + "datamap:read", + "dataset:create_or_update", + "dataset:delete", + "dataset:read", + "encryption:exec", + "evaluation:create", + "evaluation:delete", + "evaluation:read", + "evaluation:update", + "fides_taxonomy:update", + "generate:exec", + "masking:exec", + "masking:read", + "messaging:read", + "organization:create", + "organization:delete", + "organization:read", + "organization:update", + "policy:create_or_update", + "policy:delete", + "policy:read", + "privacy-experience:create", + "privacy-experience:read", + "privacy-experience:update", + "privacy-notice:create", + "privacy-notice:read", + "privacy-notice:update", + "privacy-preference-history:read", + "privacy-request-notifications:read", + "privacy-request:create", + "privacy-request:delete", + "privacy-request:read", + "privacy-request:resume", + "privacy-request:review", + "privacy-request:transfer", + "privacy-request:upload_data", + "privacy-request:view_data", + "registry:create", + "registry:delete", + "registry:read", + "registry:update", + "rule:create_or_update", + "rule:delete", + "rule:read", + "saas_config:create_or_update", + "saas_config:delete", + "saas_config:read", + "scope:read", + "storage:read", + "system:create", + "system:delete", + "system:read", + "system:update", + "system_manager:delete", + "system_manager:read", + "system_manager:update", + "system_scan:create", + "system_scan:read", + "taxonomy:create", + "taxonomy:delete", + "taxonomy:update", + "user-permission:create", + "user-permission:read", + "user-permission:update", + "user:create", + "user:delete", + "user:password-reset", + "user:read", + "user:update", + "validate:exec", + "webhook:create_or_update", + "webhook:delete", + "webhook:read" + ] +} diff --git a/clients/admin-ui/cypress/fixtures/user-management/permissions.json b/clients/admin-ui/cypress/fixtures/user-management/permissions.json index 31a3e94510..ac24589852 100644 --- a/clients/admin-ui/cypress/fixtures/user-management/permissions.json +++ b/clients/admin-ui/cypress/fixtures/user-management/permissions.json @@ -1,142 +1,145 @@ { - "roles": [ - "owner" - ], - "id": "fidesadmin", - "user_id": "fidesadmin", - "total_scopes": [ - "allow_list:create", - "allow_list:delete", - "allow_list:read", - "allow_list:update", - "classify_instance:create", - "classify_instance:read", - "classify_instance:update", - "cli-objects:create", - "cli-objects:delete", - "cli-objects:read", - "cli-objects:update", - "client:create", - "client:delete", - "client:read", - "client:update", - "config:read", - "config:update", - "connection:authorize", - "connection:create_or_update", - "connection:delete", - "connection:instantiate", - "connection:read", - "connection_type:read", - "connector_template:register", - "consent:read", - "ctl_dataset:create", - "ctl_dataset:delete", - "ctl_dataset:read", - "ctl_dataset:update", - "ctl_policy:create", - "ctl_policy:delete", - "ctl_policy:read", - "ctl_policy:update", - "custom_field:create", - "custom_field:delete", - "custom_field:read", - "custom_field:update", - "custom_field_definition:create", - "custom_field_definition:delete", - "custom_field_definition:read", - "custom_field_definition:update", - "data_category:create", - "data_category:delete", - "data_category:read", - "data_category:update", - "data_qualifier:create", - "data_qualifier:delete", - "data_qualifier:read", - "data_qualifier:update", - "data_subject:create", - "data_subject:delete", - "data_subject:read", - "data_subject:update", - "data_use:create", - "data_use:delete", - "data_use:read", - "data_use:update", - "database:reset", - "datamap:read", - "dataset:create_or_update", - "dataset:delete", - "dataset:read", - "encryption:exec", - "evaluation:create", - "evaluation:delete", - "evaluation:read", - "evaluation:update", - "fides_taxonomy:update", - "generate:exec", - "masking:exec", - "masking:read", - "messaging:create_or_update", - "messaging:delete", - "messaging:read", - "organization:create", - "organization:delete", - "organization:read", - "organization:update", - "policy:create_or_update", - "policy:delete", - "policy:read", - "privacy-notice:create", - "privacy-notice:read", - "privacy-notice:update", - "privacy-request-notifications:create_or_update", - "privacy-request-notifications:read", - "privacy-request:create", - "privacy-request:delete", - "privacy-request:read", - "privacy-request:resume", - "privacy-request:review", - "privacy-request:transfer", - "privacy-request:upload_data", - "privacy-request:view_data", - "registry:create", - "registry:delete", - "registry:read", - "registry:update", - "rule:create_or_update", - "rule:delete", - "rule:read", - "saas_config:create_or_update", - "saas_config:delete", - "saas_config:read", - "scope:read", - "storage:create_or_update", - "storage:delete", - "storage:read", - "system:create", - "system:delete", - "system:read", - "system:update", - "system_manager:delete", - "system_manager:read", - "system_manager:update", - "system_scan:create", - "system_scan:read", - "taxonomy:create", - "taxonomy:delete", - "taxonomy:update", - "user-permission:assign_owners", - "user-permission:create", - "user-permission:read", - "user-permission:update", - "user:create", - "user:delete", - "user:password-reset", - "user:read", - "user:update", - "validate:exec", - "webhook:create_or_update", - "webhook:delete", - "webhook:read" - ] -} \ No newline at end of file + "roles": ["owner"], + "id": "fidesadmin", + "user_id": "fidesadmin", + "total_scopes": [ + "allow_list:create", + "allow_list:delete", + "allow_list:read", + "allow_list:update", + "classify_instance:create", + "classify_instance:read", + "classify_instance:update", + "cli-objects:create", + "cli-objects:delete", + "cli-objects:read", + "cli-objects:update", + "client:create", + "client:delete", + "client:read", + "client:update", + "config:read", + "config:update", + "connection:authorize", + "connection:create_or_update", + "connection:delete", + "connection:instantiate", + "connection:read", + "connection_type:read", + "connector_template:register", + "consent:read", + "ctl_dataset:create", + "ctl_dataset:delete", + "ctl_dataset:read", + "ctl_dataset:update", + "ctl_policy:create", + "ctl_policy:delete", + "ctl_policy:read", + "ctl_policy:update", + "current-privacy-preference:read", + "custom_field:create", + "custom_field:delete", + "custom_field:read", + "custom_field:update", + "custom_field_definition:create", + "custom_field_definition:delete", + "custom_field_definition:read", + "custom_field_definition:update", + "data_category:create", + "data_category:delete", + "data_category:read", + "data_category:update", + "data_qualifier:create", + "data_qualifier:delete", + "data_qualifier:read", + "data_qualifier:update", + "data_subject:create", + "data_subject:delete", + "data_subject:read", + "data_subject:update", + "data_use:create", + "data_use:delete", + "data_use:read", + "data_use:update", + "database:reset", + "datamap:read", + "dataset:create_or_update", + "dataset:delete", + "dataset:read", + "encryption:exec", + "evaluation:create", + "evaluation:delete", + "evaluation:read", + "evaluation:update", + "fides_taxonomy:update", + "generate:exec", + "masking:exec", + "masking:read", + "messaging:create_or_update", + "messaging:delete", + "messaging:read", + "organization:create", + "organization:delete", + "organization:read", + "organization:update", + "policy:create_or_update", + "policy:delete", + "policy:read", + "privacy-experience:create", + "privacy-experience:read", + "privacy-experience:update", + "privacy-notice:create", + "privacy-notice:read", + "privacy-notice:update", + "privacy-preference-history:read", + "privacy-request-notifications:create_or_update", + "privacy-request-notifications:read", + "privacy-request:create", + "privacy-request:delete", + "privacy-request:read", + "privacy-request:resume", + "privacy-request:review", + "privacy-request:transfer", + "privacy-request:upload_data", + "privacy-request:view_data", + "registry:create", + "registry:delete", + "registry:read", + "registry:update", + "rule:create_or_update", + "rule:delete", + "rule:read", + "saas_config:create_or_update", + "saas_config:delete", + "saas_config:read", + "scope:read", + "storage:create_or_update", + "storage:delete", + "storage:read", + "system:create", + "system:delete", + "system:read", + "system:update", + "system_manager:delete", + "system_manager:read", + "system_manager:update", + "system_scan:create", + "system_scan:read", + "taxonomy:create", + "taxonomy:delete", + "taxonomy:update", + "user-permission:assign_owners", + "user-permission:create", + "user-permission:read", + "user-permission:update", + "user:create", + "user:delete", + "user:password-reset", + "user:read", + "user:update", + "validate:exec", + "webhook:create_or_update", + "webhook:delete", + "webhook:read" + ] +} diff --git a/clients/admin-ui/src/app/store.ts b/clients/admin-ui/src/app/store.ts index 7c17e22deb..78452b5f9b 100644 --- a/clients/admin-ui/src/app/store.ts +++ b/clients/admin-ui/src/app/store.ts @@ -32,6 +32,7 @@ import { datamapSlice } from "~/features/datamap"; import { datasetSlice } from "~/features/dataset"; import { datastoreConnectionSlice } from "~/features/datastore-connections"; import { organizationSlice } from "~/features/organization"; +import { privacyExperienceSlice } from "~/features/privacy-experience/privacy-experience.slice"; import { privacyNoticesSlice } from "~/features/privacy-notices/privacy-notices.slice"; import { subjectRequestsSlice } from "~/features/privacy-requests"; import { systemSlice } from "~/features/system"; @@ -79,6 +80,7 @@ const reducer = { [featuresSlice.name]: featuresSlice.reducer, [organizationSlice.name]: organizationSlice.reducer, [privacyNoticesSlice.name]: privacyNoticesSlice.reducer, + [privacyExperienceSlice.name]: privacyExperienceSlice.reducer, [subjectRequestsSlice.name]: subjectRequestsSlice.reducer, [systemSlice.name]: systemSlice.reducer, [taxonomySlice.name]: taxonomySlice.reducer, diff --git a/clients/admin-ui/src/features/common/api.slice.ts b/clients/admin-ui/src/features/common/api.slice.ts index 798c4ab09c..4735ca3601 100644 --- a/clients/admin-ui/src/features/common/api.slice.ts +++ b/clients/admin-ui/src/features/common/api.slice.ts @@ -37,6 +37,7 @@ export const baseApi = createApi({ "Notification", "Organization", "Plus", + "Privacy Experiences", "Privacy Notices", "System", "Request", diff --git a/clients/admin-ui/src/features/common/nav/v2/NavSideBar.tsx b/clients/admin-ui/src/features/common/nav/v2/NavSideBar.tsx index f39d92cd9b..3184c8a5a9 100644 --- a/clients/admin-ui/src/features/common/nav/v2/NavSideBar.tsx +++ b/clients/admin-ui/src/features/common/nav/v2/NavSideBar.tsx @@ -1,5 +1,5 @@ import { NavList } from "@fidesui/components"; -import { Heading, UnorderedList, VStack } from "@fidesui/react"; +import { Heading, ListItem, UnorderedList, VStack } from "@fidesui/react"; import { useRouter } from "next/router"; import React from "react"; @@ -21,13 +21,11 @@ const NavListItem = ({ {title} {children.length ? ( - + {children.map((childRoute) => ( - + + + ))} ) : null} diff --git a/clients/admin-ui/src/features/common/nav/v2/nav-config.test.ts b/clients/admin-ui/src/features/common/nav/v2/nav-config.test.ts index 652c7766be..fb04f77367 100644 --- a/clients/admin-ui/src/features/common/nav/v2/nav-config.test.ts +++ b/clients/admin-ui/src/features/common/nav/v2/nav-config.test.ts @@ -19,6 +19,8 @@ const ALL_SCOPES = [ ScopeRegistryEnum.DATA_CATEGORY_CREATE, ScopeRegistryEnum.ORGANIZATION_READ, ScopeRegistryEnum.ORGANIZATION_UPDATE, + ScopeRegistryEnum.PRIVACY_NOTICE_READ, + ScopeRegistryEnum.PRIVACY_EXPERIENCE_READ, ]; describe("configureNavGroups", () => { @@ -201,6 +203,7 @@ describe("findActiveNav", () => { config: NAV_CONFIG, hasPlus: true, userScopes: ALL_SCOPES, + flags: { privacyNotices: true, privacyExperience: true }, }); const testCases = [ @@ -241,6 +244,26 @@ describe("findActiveNav", () => { path: routes.DATASTORE_CONNECTION_ROUTE, }, }, + // Nested side nav child + { + path: routes.PRIVACY_EXPERIENCE_ROUTE, + expected: { + title: "Privacy requests", + // this _might_ not be the right thing to expect, but it at least works intuitively + // since then both the Consent route and the Privacy experience route will be marked as "active" + // since they both start with "/consent". if we see weird behavior with which nav is active + // we may need to revisit the logic in `findActiveNav` + path: routes.CONSENT_ROUTE, + }, + }, + // Parent side nav + { + path: routes.CONSENT_ROUTE, + expected: { + title: "Privacy requests", + path: routes.CONSENT_ROUTE, + }, + }, ] as const; testCases.forEach(({ path, expected }) => { diff --git a/clients/admin-ui/src/features/common/nav/v2/nav-config.ts b/clients/admin-ui/src/features/common/nav/v2/nav-config.ts index 367fa47bb1..8c9fca5ed0 100644 --- a/clients/admin-ui/src/features/common/nav/v2/nav-config.ts +++ b/clients/admin-ui/src/features/common/nav/v2/nav-config.ts @@ -53,11 +53,13 @@ export const NAV_CONFIG: NavConfigGroup[] = [ }, { title: "Consent", - // For now, we don't have a full Consent page, so just use the privacy notice route - path: routes.PRIVACY_NOTICES_ROUTE, + path: routes.CONSENT_ROUTE, requiresFlag: "privacyNotices", requiresPlus: true, - scopes: [ScopeRegistryEnum.PRIVACY_NOTICE_READ], + scopes: [ + ScopeRegistryEnum.PRIVACY_NOTICE_READ, + ScopeRegistryEnum.PRIVACY_EXPERIENCE_READ, + ], routes: [ { title: "Privacy notices", @@ -66,6 +68,13 @@ export const NAV_CONFIG: NavConfigGroup[] = [ requiresPlus: true, scopes: [ScopeRegistryEnum.PRIVACY_NOTICE_READ], }, + { + title: "Privacy experience", + path: routes.PRIVACY_EXPERIENCE_ROUTE, + requiresFlag: "privacyExperience", + requiresPlus: true, + scopes: [ScopeRegistryEnum.PRIVACY_EXPERIENCE_READ], + }, ], }, ], diff --git a/clients/admin-ui/src/features/common/nav/v2/routes.ts b/clients/admin-ui/src/features/common/nav/v2/routes.ts index d359d7b287..c6831c6549 100644 --- a/clients/admin-ui/src/features/common/nav/v2/routes.ts +++ b/clients/admin-ui/src/features/common/nav/v2/routes.ts @@ -7,7 +7,9 @@ export const CLASSIFY_SYSTEMS_ROUTE = "/classify-systems"; export const DATASET_ROUTE = "/dataset"; // Privacy requests group +export const CONSENT_ROUTE = "/consent"; export const DATASTORE_CONNECTION_ROUTE = "/datastore-connection"; +export const PRIVACY_EXPERIENCE_ROUTE = "/consent/privacy-experience"; export const PRIVACY_NOTICES_ROUTE = "/consent/privacy-notices"; export const PRIVACY_REQUESTS_ROUTE = "/privacy-requests"; export const PRIVACY_REQUESTS_CONFIGURATION_ROUTE = `${PRIVACY_REQUESTS_ROUTE}/configure`; diff --git a/clients/admin-ui/src/features/common/table/FidesTable.tsx b/clients/admin-ui/src/features/common/table/FidesTable.tsx index faa2911b04..182d55424c 100644 --- a/clients/admin-ui/src/features/common/table/FidesTable.tsx +++ b/clients/admin-ui/src/features/common/table/FidesTable.tsx @@ -124,6 +124,7 @@ export const FidesTable = ({ prepareRow(row); const { key: rowKey, ...rowProps } = row.getRowProps(); const rowName = row.original.name; + const rowId = row.original.id; return ( ({ ? { backgroundColor: "gray.50", cursor: "pointer" } : undefined } - data-testid={`row-${rowName}`} + data-testid={`row-${rowName ?? rowId}`} > {row.cells.map((cell) => { const { key: cellKey, ...cellProps } = cell.getCellProps(); diff --git a/clients/admin-ui/src/features/privacy-experience/PrivacyExperiencesTable.tsx b/clients/admin-ui/src/features/privacy-experience/PrivacyExperiencesTable.tsx new file mode 100644 index 0000000000..d83a23e979 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-experience/PrivacyExperiencesTable.tsx @@ -0,0 +1,102 @@ +import { Button, Flex, Spinner } from "@fidesui/react"; +import { PRIVACY_EXPERIENCE_ROUTE, SYSTEM_ROUTE } from "common/nav/v2/routes"; +import { useHasPermission } from "common/Restrict"; +import { DateCell, FidesTable, MultiTagCell } from "common/table"; +import EmptyTableState from "common/table/EmptyTableState"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; +import React, { useMemo } from "react"; +import { Column } from "react-table"; + +import { useAppSelector } from "~/app/hooks"; +import { + ComponentCell, + EnablePrivacyExperienceCell, +} from "~/features/privacy-experience/cells"; +import { + selectAllPrivacyExperiences, + selectPage, + selectPageSize, + useGetAllPrivacyExperiencesQuery, +} from "~/features/privacy-experience/privacy-experience.slice"; +import { PrivacyExperienceResponse, ScopeRegistryEnum } from "~/types/api"; + +const PrivacyExperiencesTable = () => { + const router = useRouter(); + // Subscribe to get all privacy experiences + const page = useAppSelector(selectPage); + const pageSize = useAppSelector(selectPageSize); + const { isLoading } = useGetAllPrivacyExperiencesQuery({ + page, + size: pageSize, + }); + const privacyExperiences = useAppSelector(selectAllPrivacyExperiences); + // Permissions + const userCanUpdate = useHasPermission([ + ScopeRegistryEnum.PRIVACY_EXPERIENCE_UPDATE, + ]); + const handleRowClick = ({ id }: PrivacyExperienceResponse) => { + if (userCanUpdate) { + router.push(`${PRIVACY_EXPERIENCE_ROUTE}/${id}`); + } + }; + + const columns: Column[] = useMemo( + () => [ + { Header: "Location", accessor: "regions", Cell: MultiTagCell }, + { + Header: "Component", + accessor: "component", + Cell: ComponentCell, + }, + { Header: "Last update", accessor: "updated_at", Cell: DateCell }, + { + Header: "Enable", + accessor: "disabled", + disabled: !userCanUpdate, + Cell: EnablePrivacyExperienceCell, + }, + ], + [userCanUpdate] + ); + + if (isLoading) { + return ( + + + + ); + } + + if (privacyExperiences.length === 0) { + return ( + + Set up data uses + + } + /> + ); + } + return ( + + columns={columns} + data={privacyExperiences} + onRowClick={userCanUpdate ? handleRowClick : undefined} + /> + ); +}; + +export default PrivacyExperiencesTable; diff --git a/clients/admin-ui/src/features/privacy-experience/cells.tsx b/clients/admin-ui/src/features/privacy-experience/cells.tsx new file mode 100644 index 0000000000..53715a3ec8 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-experience/cells.tsx @@ -0,0 +1,50 @@ +import { Text } from "@fidesui/react"; +import React from "react"; +import { CellProps } from "react-table"; + +import { EnableCell } from "~/features/common/table/"; +import { PrivacyExperienceResponse } from "~/types/api"; + +import { COMPONENT_MAP } from "./constants"; +import { usePatchPrivacyExperienceMutation } from "./privacy-experience.slice"; + +export const ComponentCell = ({ + value, +}: CellProps) => ( + {COMPONENT_MAP.get(value) ?? value} +); + +export const EnablePrivacyExperienceCell = ( + cellProps: CellProps +) => { + const [patchExperienceMutationTrigger] = usePatchPrivacyExperienceMutation(); + + const { row } = cellProps; + const onToggle = async (toggle: boolean) => { + await patchExperienceMutationTrigger([ + { + id: row.original.id, + disabled: !toggle, + }, + ]); + }; + + const { regions } = row.original; + const multipleRegions = regions ? regions.length > 1 : false; + + const title = multipleRegions + ? "Disabling multiple states" + : "Disabling experience"; + const message = multipleRegions + ? "Warning, you are about to disable this privacy experience for multiple states. If you continue, your privacy notices will not be accessible to users in these locations." + : "Warning, you are about to disable this privacy experience. If you continue, your privacy notices will not be accessible to users in this location."; + + return ( + + {...cellProps} + onToggle={onToggle} + title={title} + message={message} + /> + ); +}; diff --git a/clients/admin-ui/src/features/privacy-experience/constants.ts b/clients/admin-ui/src/features/privacy-experience/constants.ts new file mode 100644 index 0000000000..d9427900c4 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-experience/constants.ts @@ -0,0 +1,4 @@ +export const COMPONENT_MAP = new Map([ + ["overlay", "Overlay"], + ["privacy_center", "Privacy center"], +]); diff --git a/clients/admin-ui/src/features/privacy-experience/privacy-experience.slice.ts b/clients/admin-ui/src/features/privacy-experience/privacy-experience.slice.ts new file mode 100644 index 0000000000..9b51350830 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-experience/privacy-experience.slice.ts @@ -0,0 +1,109 @@ +import { createSelector, createSlice } from "@reduxjs/toolkit"; + +import type { RootState } from "~/app/store"; +import { baseApi } from "~/features/common/api.slice"; +import { + Page_PrivacyExperienceResponse_, + PrivacyExperience, + PrivacyExperienceResponse, + PrivacyNoticeRegion, +} from "~/types/api"; + +export interface State { + page?: number; + pageSize?: number; +} + +const initialState: State = { + page: 1, + pageSize: 50, +}; + +interface PrivacyExperienceParams { + show_disabled?: boolean; + region?: PrivacyNoticeRegion; + page?: number; + size?: number; +} + +const privacyExperienceApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + getAllPrivacyExperiences: build.query< + Page_PrivacyExperienceResponse_, + PrivacyExperienceParams + >({ + query: (params) => ({ + url: `privacy-experience/`, + params: { ...params, show_disabled: true }, + }), + providesTags: () => ["Privacy Experiences"], + }), + patchPrivacyExperience: build.mutation< + PrivacyExperienceResponse[], + Partial[] + >({ + query: (payload) => ({ + method: "PATCH", + url: `privacy-experience/`, + body: payload, + }), + invalidatesTags: () => ["Privacy Experiences"], + }), + getPrivacyExperienceById: build.query({ + query: (id) => ({ + url: `privacy-experience/${id}`, + }), + providesTags: (result, error, arg) => [ + { type: "Privacy Experiences", id: arg }, + ], + }), + postPrivacyExperience: build.mutation({ + query: (payload) => ({ + method: "POST", + url: `privacy-experience/`, + body: payload, + }), + invalidatesTags: () => ["Privacy Experiences"], + }), + }), +}); + +export const { + useGetAllPrivacyExperiencesQuery, + usePatchPrivacyExperienceMutation, + useGetPrivacyExperienceByIdQuery, + usePostPrivacyExperienceMutation, +} = privacyExperienceApi; + +export const privacyExperienceSlice = createSlice({ + name: "privacyExperience", + initialState, + reducers: {}, +}); + +export const { reducer } = privacyExperienceSlice; + +const selectPrivacyExperiences = (state: RootState) => state.privacyExperience; +export const selectPage = createSelector( + selectPrivacyExperiences, + (state) => state.page +); + +export const selectPageSize = createSelector( + selectPrivacyExperiences, + (state) => state.pageSize +); + +const emptyPrivacyExperiences: PrivacyExperienceResponse[] = []; +export const selectAllPrivacyExperiences = createSelector( + [(RootState) => RootState, selectPage, selectPageSize], + (RootState, page, pageSize) => { + const data = privacyExperienceApi.endpoints.getAllPrivacyExperiences.select( + { + page, + size: pageSize, + } + )(RootState)?.data; + return data ? data.items : emptyPrivacyExperiences; + } +); diff --git a/clients/admin-ui/src/features/privacy-notices/privacy-notices.slice.ts b/clients/admin-ui/src/features/privacy-notices/privacy-notices.slice.ts index 4a0882243c..cecb584ccf 100644 --- a/clients/admin-ui/src/features/privacy-notices/privacy-notices.slice.ts +++ b/clients/admin-ui/src/features/privacy-notices/privacy-notices.slice.ts @@ -17,7 +17,7 @@ export interface State { const initialState: State = { page: 1, - pageSize: 10, + pageSize: 50, }; interface PrivacyNoticesParams { diff --git a/clients/admin-ui/src/flags.json b/clients/admin-ui/src/flags.json index 0fbb97ced2..5b415c8a0d 100644 --- a/clients/admin-ui/src/flags.json +++ b/clients/admin-ui/src/flags.json @@ -23,5 +23,12 @@ "development": true, "test": true, "production": false + }, + "privacyExperience": { + "description": "Page to configure privacy experience", + "development": true, + "test": true, + "production": false, + "userCannotModify": true } } diff --git a/clients/admin-ui/src/pages/consent/index.tsx b/clients/admin-ui/src/pages/consent/index.tsx new file mode 100644 index 0000000000..0826ed76e2 --- /dev/null +++ b/clients/admin-ui/src/pages/consent/index.tsx @@ -0,0 +1,24 @@ +import { Center, Spinner } from "@fidesui/react"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +import Layout from "~/features/common/Layout"; +import { PRIVACY_NOTICES_ROUTE } from "~/features/common/nav/v2/routes"; + +const ConsentPage = () => { + const router = useRouter(); + + useEffect(() => { + router.push(PRIVACY_NOTICES_ROUTE); + }, [router]); + + return ( + +
+ +
+
+ ); +}; + +export default ConsentPage; diff --git a/clients/admin-ui/src/pages/consent/privacy-experience/[id].tsx b/clients/admin-ui/src/pages/consent/privacy-experience/[id].tsx new file mode 100644 index 0000000000..5998bb7132 --- /dev/null +++ b/clients/admin-ui/src/pages/consent/privacy-experience/[id].tsx @@ -0,0 +1,107 @@ +import { PRIVACY_REQUESTS_ROUTE } from "@fidesui/components"; +import { + Box, + Breadcrumb, + BreadcrumbItem, + Center, + Heading, + Spinner, + Text, +} from "@fidesui/react"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; + +import Layout from "~/features/common/Layout"; +import { PRIVACY_EXPERIENCE_ROUTE } from "~/features/common/nav/v2/routes"; +import { COMPONENT_MAP } from "~/features/privacy-experience/constants"; +import { useGetPrivacyExperienceByIdQuery } from "~/features/privacy-experience/privacy-experience.slice"; +import { ComponentType } from "~/types/api"; + +const PrivacyExperienceDetailPage = () => { + const router = useRouter(); + + let experienceId = ""; + if (router.query.id) { + experienceId = Array.isArray(router.query.id) + ? router.query.id[0] + : router.query.id; + } + + const { data, isLoading } = useGetPrivacyExperienceByIdQuery(experienceId); + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (!data) { + return ( + + No privacy experience with id {experienceId} found. + + ); + } + + const header = + data.component === ComponentType.OVERLAY + ? "Configure your consent overlay" + : "Configure your privacy center"; + + return ( + + + + {header} + + + + + + Privacy requests + + + {/* TODO: Add Consent breadcrumb once the page exists */} + + + Privacy experience + + + + + {COMPONENT_MAP.get(data.component) ?? data.component} + + + + + + + + Configure the text of your privacy overlay, privacy banner, and the + text of the buttons which users will click to accept, reject, manage, + and save their preferences. This privacy overlay contains opt-in + privacy notices and must be delivered with a banner. + + + Work in progress! + + + + ); +}; + +export default PrivacyExperienceDetailPage; diff --git a/clients/admin-ui/src/pages/consent/privacy-experience/index.tsx b/clients/admin-ui/src/pages/consent/privacy-experience/index.tsx new file mode 100644 index 0000000000..585d86a8b0 --- /dev/null +++ b/clients/admin-ui/src/pages/consent/privacy-experience/index.tsx @@ -0,0 +1,50 @@ +import { PRIVACY_REQUESTS_ROUTE } from "@fidesui/components"; +import { Box, Breadcrumb, BreadcrumbItem, Heading, Text } from "@fidesui/react"; +import NextLink from "next/link"; +import React from "react"; + +import Layout from "~/features/common/Layout"; +import { PRIVACY_EXPERIENCE_ROUTE } from "~/features/common/nav/v2/routes"; +import PrivacyExperiencesTable from "~/features/privacy-experience/PrivacyExperiencesTable"; + +const PrivacyExperiencePage = () => ( + + + + Privacy experience + + + + + Privacy requests + + {/* TODO: Add Consent breadcrumb once the page exists */} + + + Privacy experience + + + + + + + Based on your privacy notices, Fides has created the overlay and privacy + experience configuration below. Your privacy notices will be presented by + region in these components. Edit each component to adjust the text that + displays in the privacy center, overlay, and banners that show your + notices. When you’re ready to include these privacy notices on your + website, copy the javascript using the button on this page and place it on + your website. + + + + + +); + +export default PrivacyExperiencePage; diff --git a/clients/admin-ui/src/types/api/index.ts b/clients/admin-ui/src/types/api/index.ts index 00bda557a7..da56a8270e 100644 --- a/clients/admin-ui/src/types/api/index.ts +++ b/clients/admin-ui/src/types/api/index.ts @@ -204,7 +204,9 @@ export type { PolicyWebhookUpdateResponse } from "./models/PolicyWebhookUpdateRe export type { PostgreSQLDocsSchema } from "./models/PostgreSQLDocsSchema"; export type { PrivacyDeclaration } from "./models/PrivacyDeclaration"; export type { PrivacyDeclarationResponse } from "./models/PrivacyDeclarationResponse"; +export type { PrivacyExperience } from "./models/PrivacyExperience"; export type { PrivacyExperienceResponse } from "./models/PrivacyExperienceResponse"; +export type { PrivacyExperienceWithId } from "./models/PrivacyExperienceWithId"; export type { PrivacyNoticeCreation } from "./models/PrivacyNoticeCreation"; export type { PrivacyNoticeHistorySchema } from "./models/PrivacyNoticeHistorySchema"; export { PrivacyNoticeRegion } from "./models/PrivacyNoticeRegion"; diff --git a/clients/admin-ui/src/types/api/models/PrivacyExperience.ts b/clients/admin-ui/src/types/api/models/PrivacyExperience.ts new file mode 100644 index 0000000000..4093912a7f --- /dev/null +++ b/clients/admin-ui/src/types/api/models/PrivacyExperience.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ComponentType } from "./ComponentType"; +import type { DeliveryMechanism } from "./DeliveryMechanism"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; + +/** + * Base for PrivacyExperience API objects + */ +export type PrivacyExperience = { + disabled?: boolean; + component: ComponentType; + delivery_mechanism: DeliveryMechanism; + regions: Array; + component_title?: string; + component_description?: string; + banner_title?: string; + banner_description?: string; + link_label?: string; + confirmation_button_label?: string; + reject_button_label?: string; + acknowledgement_button_label?: string; +}; diff --git a/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts b/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts index bea7841f57..7fdb87afa9 100644 --- a/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts +++ b/clients/admin-ui/src/types/api/models/PrivacyExperienceResponse.ts @@ -14,7 +14,7 @@ export type PrivacyExperienceResponse = { disabled?: boolean; component: ComponentType; delivery_mechanism: DeliveryMechanism; - regions?: Array; + regions: Array; component_title?: string; component_description?: string; banner_title?: string; diff --git a/clients/admin-ui/src/types/api/models/PrivacyExperienceWithId.ts b/clients/admin-ui/src/types/api/models/PrivacyExperienceWithId.ts new file mode 100644 index 0000000000..9036752119 --- /dev/null +++ b/clients/admin-ui/src/types/api/models/PrivacyExperienceWithId.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ComponentType } from "./ComponentType"; +import type { DeliveryMechanism } from "./DeliveryMechanism"; +import type { PrivacyNoticeRegion } from "./PrivacyNoticeRegion"; + +/** + * An API representation of a PrivacyExperience that includes an `id` field. + * Used to help model API responses and update payloads + */ +export type PrivacyExperienceWithId = { + disabled?: boolean; + component: ComponentType; + delivery_mechanism: DeliveryMechanism; + regions: Array; + component_title?: string; + component_description?: string; + banner_title?: string; + banner_description?: string; + link_label?: string; + confirmation_button_label?: string; + reject_button_label?: string; + acknowledgement_button_label?: string; + id: string; +};