diff --git a/CHANGELOG.md b/CHANGELOG.md index cbcf920b2c..b2419e6d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o ### Added - Fides GTM & event origination [#5821](https://github.com/ethyca/fides/pull/5821) +- Added a consent reporting table and consent lookup feature [#5839](https://github.com/ethyca/fides/pull/5839) ### Fixed - Addressed TCModel console error when opting into some purposes [#5850](https://github.com/ethyca/fides/pull/5850) diff --git a/clients/admin-ui/cypress/e2e/consent-reporting.cy.ts b/clients/admin-ui/cypress/e2e/consent-reporting.cy.ts index c31460e863..e8323a728a 100644 --- a/clients/admin-ui/cypress/e2e/consent-reporting.cy.ts +++ b/clients/admin-ui/cypress/e2e/consent-reporting.cy.ts @@ -40,7 +40,7 @@ describe("Consent reporting", () => { }); }); - describe("downloading reports", () => { + describe("results view and download report", () => { beforeEach(() => { stubPlus(true, { core_fides_version: "1.9.6", @@ -70,10 +70,80 @@ describe("Consent reporting", () => { url: "/api/v1/plus/consent_reporting*", method: "GET", }).as("getConsentReport"); - cy.getByTestId("input-from-date").type("2023-11-01"); - cy.getByTestId("input-to-date").type("2023-11-07"); + cy.getByTestId("input-date-range").first().type("2023-11-01"); + cy.getByTestId("input-date-range").last().type("2023-11-07"); cy.getByTestId("download-btn").click(); + cy.getByTestId("download-report-btn").click(); cy.wait("@getConsentReport"); }); + it("can lookup specific consent preferences", () => { + cy.intercept({ + url: "/api/v1/current-privacy-preferences*", + method: "GET", + }).as("lookupConsentPreferences"); + cy.getByTestId("consent-reporting-dropdown-btn").click(); + cy.getByTestId("consent-preference-lookup-btn").click(); + cy.getByTestId("subject-search-input").type("test@example.com{enter}"); + cy.wait("@lookupConsentPreferences").then((interception) => { + const { url: requestUrl } = interception.request; + let url = new URL(requestUrl); + let params = new URLSearchParams(url.search); + expect(params.get("email")).to.equal("test@example.com"); + expect(params.get("phone_number")).to.equal("test@example.com"); + expect(params.get("fides_user_device_id")).to.equal("test@example.com"); + expect(params.get("external_id")).to.equal("test@example.com"); + }); + }); + it("loads the consent report table without date filters", () => { + cy.intercept( + { + url: "/api/v1/historical-privacy-preferences*", + method: "GET", + }, + { + fixture: "consent-reporting/historical-privacy-preferences.json", + }, + ).as("getConsentReport"); + + cy.wait("@getConsentReport").then((interception) => { + const { url: requestUrl } = interception.request; + let url = new URL(requestUrl); + let params = new URLSearchParams(url.search); + expect(params.get("request_timestamp_gt")).to.be.null; + expect(params.get("request_timestamp_lt")).to.be.null; + }); + + cy.getByTestId("fidesTable-body").children().should("have.length", 22); + }); + it("loads the consent report table with date filters", () => { + cy.intercept( + { + url: "/api/v1/historical-privacy-preferences*", + method: "GET", + }, + { + fixture: "consent-reporting/historical-privacy-preferences.json", + }, + ).as("getConsentReport"); + + cy.wait("@getConsentReport"); + + cy.getByTestId("input-date-range").first().type("2023-11-01"); + cy.getByTestId("input-date-range").last().type("2023-11-07{enter}"); + + cy.wait("@getConsentReport").then((interception) => { + const { url: requestUrl } = interception.request; + let url = new URL(requestUrl); + let params = new URLSearchParams(url.search); + expect(params.get("request_timestamp_gt")).to.equal( + "2023-11-01T00:00:00.000Z", + ); + expect(params.get("request_timestamp_lt")).to.equal( + "2023-11-07T23:59:59.999Z", + ); + }); + + cy.getByTestId("fidesTable-body").children().should("have.length", 22); + }); }); }); diff --git a/clients/admin-ui/cypress/fixtures/consent-reporting/historical-privacy-preferences.json b/clients/admin-ui/cypress/fixtures/consent-reporting/historical-privacy-preferences.json new file mode 100644 index 0000000000..32868496b1 --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/consent-reporting/historical-privacy-preferences.json @@ -0,0 +1,678 @@ +{ + "items": [ + { + "id": "pri_464f8970-68a7-4906-acb5-773334b7fd6c", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "65c78e68-d932-45ca-9d20-8903d8941808", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T21:49:46.914848Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": null, + "preference": "tcf", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_ef4a40e2-b004-4a4a-b2b4-3836b32b9759", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "292a9df6-e750-49e3-85f5-b6ab7e6fce7a", + "notice_name": "tcf", + "tcf_preferences": { + "feature_preferences": [], + "system_consent_preferences": [ + { + "id": "ctl_73e94702-0f81-43ff-924b-f5e21f311e0d", + "preference": "opt_out" + }, + { + "id": "ctl_62e293f1-c87a-4415-87ca-03dcea7922f0", + "preference": "opt_out" + } + ], + "vendor_consent_preferences": [], + "purpose_consent_preferences": [ + { + "id": 1, + "preference": "opt_in" + }, + { + "id": 3, + "preference": "opt_in" + } + ], + "special_feature_preferences": [], + "special_purpose_preferences": [], + "system_legitimate_interests_preferences": [ + { + "id": "ctl_62e293f1-c87a-4415-87ca-03dcea7922f0", + "preference": "opt_in" + } + ], + "vendor_legitimate_interests_preferences": [], + "purpose_legitimate_interests_preferences": [ + { + "id": 1, + "preference": "opt_out" + } + ] + }, + "property_id": null + }, + { + "id": "pri_3ceb5193-7e06-4657-b136-415b7c320059", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "e54efb81-cab0-4ce3-8078-294e9e987130", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T21:49:14.544192Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": null, + "preference": "tcf", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_ef4a40e2-b004-4a4a-b2b4-3836b32b9759", + "truncated_ip_address": "172.18.0.0", + "method": "accept", + "served_notice_history_id": "eeeab5bd-e1d9-4732-a87c-383c9c888979", + "notice_name": "tcf", + "tcf_preferences": { + "feature_preferences": [], + "system_consent_preferences": [ + { + "id": "ctl_73e94702-0f81-43ff-924b-f5e21f311e0d", + "preference": "opt_in" + }, + { + "id": "ctl_62e293f1-c87a-4415-87ca-03dcea7922f0", + "preference": "opt_in" + } + ], + "vendor_consent_preferences": [], + "purpose_consent_preferences": [ + { + "id": 1, + "preference": "opt_in" + }, + { + "id": 3, + "preference": "opt_in" + } + ], + "special_feature_preferences": [], + "special_purpose_preferences": [], + "system_legitimate_interests_preferences": [ + { + "id": "ctl_62e293f1-c87a-4415-87ca-03dcea7922f0", + "preference": "opt_in" + } + ], + "vendor_legitimate_interests_preferences": [], + "purpose_legitimate_interests_preferences": [ + { + "id": 1, + "preference": "opt_in" + } + ] + }, + "property_id": null + }, + { + "id": "pri_22076000-47f5-4794-b666-843167166094", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "108463f2-2dd4-4264-a0eb-e20f788471d0", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T21:45:19.778571Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_d23d8e45-15bf-4f19-b78d-01fcd9a65406", + "preference": "opt_out", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_e5db14b3-a0bd-4f07-9b6d-7723bae94f37", + "truncated_ip_address": "172.18.0.0", + "method": "accept", + "served_notice_history_id": "b96d343e-ea2a-470d-8f6c-8cb40b0edf41", + "notice_name": "Custom Notice Title", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_f6aaccc8-9c6f-4bd3-bbbc-1f2b18169b21", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "b2579a66-d360-4e79-bcb6-3893a44ced90", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T21:37:28.138967Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_d23d8e45-15bf-4f19-b78d-01fcd9a65406", + "preference": "opt_in", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_e5db14b3-a0bd-4f07-9b6d-7723bae94f37", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "5f215197-f4a6-4310-87f2-3f4db37852b2", + "notice_name": "Custom Notice Title", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_f5008cd4-880b-4293-bf8a-0056d7a7d2fb", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "b2579a66-d360-4e79-bcb6-3893a44ced90", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T21:36:19.929658Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_d23d8e45-15bf-4f19-b78d-01fcd9a65406", + "preference": "opt_out", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_e5db14b3-a0bd-4f07-9b6d-7723bae94f37", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "0daa2a29-365b-4087-ac03-4b34cc5bc49d", + "notice_name": "Custom Notice Title", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_6b8b59e5-3920-416d-a95a-c08a56717c8c", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "b2579a66-d360-4e79-bcb6-3893a44ced90", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T21:00:12.523177Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_d23d8e45-15bf-4f19-b78d-01fcd9a65406", + "preference": "opt_in", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_e5db14b3-a0bd-4f07-9b6d-7723bae94f37", + "truncated_ip_address": "172.18.0.0", + "method": "accept", + "served_notice_history_id": "565e6846-fa37-4ed3-943a-bbd2774609c0", + "notice_name": "Custom Notice Title", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_91086c6d-518d-4794-be67-335dc038f374", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "0e36a48e-0113-48ce-9e97-03ad934983b2", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T20:51:25.450642Z", + "request_origin": "tcf_overlay", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_d23d8e45-15bf-4f19-b78d-01fcd9a65406", + "preference": "opt_in", + "user_geography": "eea", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_e5db14b3-a0bd-4f07-9b6d-7723bae94f37", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "8a061366-14be-49f9-ad98-b966ca162b9d", + "notice_name": "Custom Notice Title", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_441dd74d-a6c9-4043-aa1c-5568f0e15379", + "privacy_request_id": null, + "email": "lucano@ethyca.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "f923c022-0a3c-4beb-b32e-c23c52f48031", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T15:35:18.912988Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_c04e7e4a-37ba-4227-9bce-42a721a3cf4e", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_6f2ec0a2-50c2-4ad8-a5aa-f4a7efdb8164", + "notice_name": "Functional", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_6188f018-131a-467b-b452-986e7a3a3d0d", + "privacy_request_id": null, + "email": "lucano@ethyca.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "f923c022-0a3c-4beb-b32e-c23c52f48031", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T15:35:18.910261Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_2ef10af7-b3a8-4b51-b828-fda0a4b70f0a", + "preference": "opt_out", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_6f2ec0a2-50c2-4ad8-a5aa-f4a7efdb8164", + "notice_name": "Analytics", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_89552fba-ea6e-48d1-8813-cf9318279ef2", + "privacy_request_id": null, + "email": "lucano@ethyca.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "f923c022-0a3c-4beb-b32e-c23c52f48031", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T15:35:18.905024Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_f55229d6-0f60-4925-9bc5-c86460c2525b", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_6f2ec0a2-50c2-4ad8-a5aa-f4a7efdb8164", + "notice_name": "Marketing", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_8e437304-d13e-49c9-91e2-13c2d850061d", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "f923c022-0a3c-4beb-b32e-c23c52f48031", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T14:22:25.986852Z", + "request_origin": "modal", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_c04e7e4a-37ba-4227-9bce-42a721a3cf4e", + "preference": "opt_out", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_21a1fbe2-96a8-41cf-8924-a0f69c2c171f", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "7dbcf25e-cca2-4090-903f-d408fbc2a62b", + "notice_name": "Functional", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_d5623ccf-0224-41b2-9133-eb956f8ef19d", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "f923c022-0a3c-4beb-b32e-c23c52f48031", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T14:22:25.982377Z", + "request_origin": "modal", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_2ef10af7-b3a8-4b51-b828-fda0a4b70f0a", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_21a1fbe2-96a8-41cf-8924-a0f69c2c171f", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "7dbcf25e-cca2-4090-903f-d408fbc2a62b", + "notice_name": "Analytics", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_c3725532-3874-4870-871b-20e7f98cdeb8", + "privacy_request_id": null, + "email": null, + "phone_number": null, + "external_id": null, + "fides_user_device_id": "f923c022-0a3c-4beb-b32e-c23c52f48031", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T14:22:25.976386Z", + "request_origin": "modal", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_f55229d6-0f60-4925-9bc5-c86460c2525b", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_21a1fbe2-96a8-41cf-8924-a0f69c2c171f", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "7dbcf25e-cca2-4090-903f-d408fbc2a62b", + "notice_name": "Marketing", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_e758ca2d-d0b7-48be-b527-27bfd5656123", + "privacy_request_id": null, + "email": "testests@testse.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T14:06:55.153275Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_c04e7e4a-37ba-4227-9bce-42a721a3cf4e", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_18d8395e-07bb-4469-b14f-4e437055a549", + "notice_name": "Functional", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_a0acdb55-a7a5-4b25-aa00-989082f01f10", + "privacy_request_id": null, + "email": "testests@testse.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T14:06:55.149770Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_2ef10af7-b3a8-4b51-b828-fda0a4b70f0a", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_18d8395e-07bb-4469-b14f-4e437055a549", + "notice_name": "Analytics", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_7803b02a-6160-492a-a23a-19cf65ffdb92", + "privacy_request_id": null, + "email": "testests@testse.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-06T14:06:55.138375Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_f55229d6-0f60-4925-9bc5-c86460c2525b", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_18d8395e-07bb-4469-b14f-4e437055a549", + "notice_name": "Marketing", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_1c8871c0-cae9-410b-8153-70f9e2466878", + "privacy_request_id": null, + "email": "lucano@ethyca.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-03T15:00:17.940096Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_c04e7e4a-37ba-4227-9bce-42a721a3cf4e", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_a4ba621c-0458-4d37-aff5-953755aab415", + "notice_name": "Functional", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_f72d77ac-13ba-4ded-a9c5-a485c1760b61", + "privacy_request_id": null, + "email": "lucano@ethyca.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-03T15:00:17.937429Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_2ef10af7-b3a8-4b51-b828-fda0a4b70f0a", + "preference": "opt_out", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_a4ba621c-0458-4d37-aff5-953755aab415", + "notice_name": "Analytics", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_4666a081-febe-4c13-9fcf-33e4beb86b57", + "privacy_request_id": null, + "email": "lucano@ethyca.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-03T15:00:17.933418Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_f55229d6-0f60-4925-9bc5-c86460c2525b", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_a4ba621c-0458-4d37-aff5-953755aab415", + "notice_name": "Marketing", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_7b4ab234-caa5-4017-b203-dde4a93b2572", + "privacy_request_id": null, + "email": "sadasd@wsadasd.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-03T14:58:36.671733Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_c04e7e4a-37ba-4227-9bce-42a721a3cf4e", + "preference": "opt_out", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_aa4f50a4-5782-4fe5-9694-0d7a2759000b", + "notice_name": "Functional", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_46c3d41a-658c-48dc-b160-99ecffb7d6de", + "privacy_request_id": null, + "email": "sadasd@wsadasd.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-03T14:58:36.668232Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_2ef10af7-b3a8-4b51-b828-fda0a4b70f0a", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_aa4f50a4-5782-4fe5-9694-0d7a2759000b", + "notice_name": "Analytics", + "tcf_preferences": null, + "property_id": null + }, + { + "id": "pri_9f53e9a2-85ab-49bc-9cf8-d68fa8fb4f52", + "privacy_request_id": null, + "email": "sadasd@wsadasd.com", + "phone_number": null, + "external_id": null, + "fides_user_device_id": "495d5847-89e4-48f9-b603-ff4986ddd873", + "secondary_user_ids": null, + "request_timestamp": "2025-03-03T14:58:36.657312Z", + "request_origin": "privacy_center", + "request_status": null, + "request_type": "consent", + "approver_id": null, + "privacy_notice_history_id": "pri_f55229d6-0f60-4925-9bc5-c86460c2525b", + "preference": "opt_in", + "user_geography": "us_ca", + "affected_system_status": {}, + "url_recorded": "http://localhost:3001/", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "experience_config_history_id": "pri_452bc61b-2a6f-463d-8027-5122f45d59ab", + "truncated_ip_address": "172.18.0.0", + "method": "save", + "served_notice_history_id": "ser_aa4f50a4-5782-4fe5-9694-0d7a2759000b", + "notice_name": "Marketing", + "tcf_preferences": null, + "property_id": null + } + ], + "total": 22, + "page": 1, + "size": 25, + "pages": 1 +} diff --git a/clients/admin-ui/package.json b/clients/admin-ui/package.json index 121a87057a..a286012bfb 100644 --- a/clients/admin-ui/package.json +++ b/clients/admin-ui/package.json @@ -41,6 +41,7 @@ "d3-hierarchy": "^3.1.2", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", + "dayjs": "^1.11.13", "fides-js": "^0.0.1", "fidesui": "*", "file-saver": "^2.0.5", diff --git a/clients/admin-ui/src/features/common/api.slice.ts b/clients/admin-ui/src/features/common/api.slice.ts index 6d3f395058..4138f6ece3 100644 --- a/clients/admin-ui/src/features/common/api.slice.ts +++ b/clients/admin-ui/src/features/common/api.slice.ts @@ -25,6 +25,9 @@ export const baseApi = createApi({ "Classify Instances Systems", "Connection Type", "Consentable Items", + "Consent Reporting", + "Consent Reporting Export", + "Current Privacy Preferences", "Custom Assets", "Custom Field Definition", "Custom Fields", @@ -66,7 +69,6 @@ export const baseApi = createApi({ "User", "Configuration Settings", "TCF Purpose Override", - "Consent Reporting", "OpenID Provider", ], endpoints: () => ({}), diff --git a/clients/admin-ui/src/features/common/table/v2/cells.tsx b/clients/admin-ui/src/features/common/table/v2/cells.tsx index 2aa95c332e..4db73e3842 100644 --- a/clients/admin-ui/src/features/common/table/v2/cells.tsx +++ b/clients/admin-ui/src/features/common/table/v2/cells.tsx @@ -7,19 +7,20 @@ import { AntSwitchProps as SwitchProps, AntTag as Tag, AntTagProps as TagProps, + AntTooltip as Tooltip, Checkbox, CheckboxProps, Flex, FlexProps, Text, TextProps, - Tooltip, useDisclosure, useToast, WarningIcon, } from "fidesui"; import { useField, useFormikContext } from "formik"; -import { ReactNode, useEffect, useMemo, useState } from "react"; +import { isBoolean } from "lodash"; +import { ReactElement, ReactNode, useEffect, useMemo, useState } from "react"; import { getErrorMessage, isErrorResult } from "~/features/common/helpers"; import ConfirmationModal from "~/features/common/modals/ConfirmationModal"; @@ -35,7 +36,7 @@ export const DefaultCell = ({ ...chakraStyleProps }: { cellProps?: FidesCellProps; - value: string | undefined | number | null | boolean; + value: string | ReactElement | undefined | number | null | boolean; } & TextProps) => { const expandable = !!cellProps?.cell.column.columnDef.meta?.showHeaderMenu; const isExpanded = expandable && !!cellProps?.cellState?.isExpanded; @@ -51,7 +52,7 @@ export const DefaultCell = ({ title={isExpanded && !!value ? undefined : value?.toString()} {...chakraStyleProps} > - {value !== null && value !== undefined ? value.toString() : value} + {isBoolean(value) ? value.toString() : value} ); }; @@ -73,7 +74,7 @@ export const RelativeTimestampCell = ({ return ( - + ( diff --git a/clients/admin-ui/src/features/common/toast.tsx b/clients/admin-ui/src/features/common/toast.tsx index f477f4b57e..536476c728 100644 --- a/clients/admin-ui/src/features/common/toast.tsx +++ b/clients/admin-ui/src/features/common/toast.tsx @@ -1,9 +1,15 @@ import { Text, UseToastOptions } from "fidesui"; import { ReactNode } from "react"; -const SuccessMessage = ({ children }: { children: ReactNode }) => ( +const SuccessMessage = ({ + children, + title = "Success", +}: { + children: ReactNode; + title?: string; +}) => ( - Success: {children} + {title}: {children} ); @@ -22,8 +28,11 @@ export const DEFAULT_TOAST_PARAMS: UseToastOptions = { isClosable: true, }; -export const successToastParams = (message: ReactNode): UseToastOptions => { - const description = {message}; +export const successToastParams = ( + message: ReactNode, + title?: string, +): UseToastOptions => { + const description = {message}; return { ...DEFAULT_TOAST_PARAMS, ...{ description } }; }; diff --git a/clients/admin-ui/src/features/consent-reporting/ConsentLookupModal.tsx b/clients/admin-ui/src/features/consent-reporting/ConsentLookupModal.tsx new file mode 100644 index 0000000000..a198c3877e --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/ConsentLookupModal.tsx @@ -0,0 +1,160 @@ +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { + AntEmpty as Empty, + AntForm as Form, + AntInput as Input, + AntTypography as Typography, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + useToast, +} from "fidesui"; +import { isEmpty } from "lodash"; +import { useEffect, useState } from "react"; + +import { + PreferencesSavedExtended, + PreferenceWithNoticeInformation, +} from "~/types/api"; + +import { getErrorMessage } from "../common/helpers"; +import { FidesTableV2 } from "../common/table/v2"; +import { useLazyGetCurrentPrivacyPreferencesQuery } from "./consent-reporting.slice"; +import useConsentLookupTableColumns from "./hooks/useConsentLookupTableColumns"; +import useTcfConsentColumns, { + TcfDetailRow, +} from "./hooks/useTcfConsentColumns"; + +interface ConsentLookupModalProps { + isOpen: boolean; + onClose: () => void; +} + +const ConsentLookupModal = ({ isOpen, onClose }: ConsentLookupModalProps) => { + const [isSearching, setIsSearching] = useState(false); + const [searchResults, setSearchResults] = useState< + PreferencesSavedExtended | undefined | null + >(); + const [getCurrentPrivacyPreferencesTrigger] = + useLazyGetCurrentPrivacyPreferencesQuery(); + + const toast = useToast(); + + useEffect(() => { + // reset state when modal is closed + if (!isOpen) { + setSearchResults(undefined); + setIsSearching(false); + } + }, [isOpen]); + + const handleSearch = async (search: string) => { + setIsSearching(true); + const { data, isError, error } = await getCurrentPrivacyPreferencesTrigger({ + search, + }); + const errorStatus = error && "status" in error && error?.status; + if (isError && errorStatus !== 404) { + const errorMsg = getErrorMessage( + error, + `A problem occurred while looking up the preferences.`, + ); + + toast({ status: "error", description: errorMsg }); + } else { + setSearchResults(data || null); + } + setIsSearching(false); + }; + + const columns = useConsentLookupTableColumns(); + const preferences = searchResults?.preferences || []; + const hasPreferences = !isEmpty(preferences); + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + data: preferences, + columns, + getRowId: (row) => `${row.privacy_notice_history_id}`, + manualPagination: true, + }); + + const { tcfColumns, mapTcfPreferencesToRowColumns } = useTcfConsentColumns(); + const tcfData = mapTcfPreferencesToRowColumns(searchResults); + const hasTcfData = !isEmpty(tcfData); + const tcfTableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + data: tcfData, + columns: tcfColumns, + getRowId: (row) => `${row.key}-${row.id}`, + manualPagination: true, + }); + + return ( + + + + + Consent preference lookup + + + Use this search to look up an individual's latest consent + record. You can search by phone number, email, or device ID to + retrieve the most recent consent preference associated with that + exact identifier. + + + Note: This is an exact match search—partial entries + or similar results will not be returned. This lookup retrieves only + the most recent consent preference, not the full consent history. + + +
+ + + +
+
+ {(!hasTcfData || hasPreferences) && ( + + tableInstance={tableInstance} + emptyTableNotice={ + + } + /> + )} + {hasTcfData && ( +
+ tableInstance={tcfTableInstance} /> +
+ )} +
+
+
+
+ ); +}; +export default ConsentLookupModal; diff --git a/clients/admin-ui/src/features/consent-reporting/ConsentReportDownloadModal.tsx b/clients/admin-ui/src/features/consent-reporting/ConsentReportDownloadModal.tsx new file mode 100644 index 0000000000..60fcf71cec --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/ConsentReportDownloadModal.tsx @@ -0,0 +1,77 @@ +import { Dayjs } from "dayjs"; +import { + AntButton as Button, + AntFlex as Flex, + AntTypography as Typography, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, +} from "fidesui"; + +import useConsentReportingDownload from "./hooks/useConsentReportingDownload"; + +interface ConsentReportDownloadModalProps { + isOpen: boolean; + onClose: () => void; + startDate: Dayjs | null; + endDate: Dayjs | null; +} + +const ConsentReportDownloadModal = ({ + isOpen, + onClose, + startDate, + endDate, +}: ConsentReportDownloadModalProps) => { + const { downloadReport, isDownloadingReport } = useConsentReportingDownload(); + const handleDownloadClicked = async () => { + await downloadReport({ + startDate, + endDate, + }); + onClose(); + }; + + return ( + + + + + Download consent report + + + The downloaded CSV may differ from the UI in Fides, including column + order and naming. + + + For large datasets, file generation may take a few minutes after + selecting "Download". + + + + + + + + + ); +}; +export default ConsentReportDownloadModal; diff --git a/clients/admin-ui/src/features/consent-reporting/ConsentReporting.tsx b/clients/admin-ui/src/features/consent-reporting/ConsentReporting.tsx deleted file mode 100644 index 760ef04f41..0000000000 --- a/clients/admin-ui/src/features/consent-reporting/ConsentReporting.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - AntButton as Button, - HStack, - Input, - InputGroup, - InputLeftAddon, - useToast, -} from "fidesui"; -import { useState } from "react"; - -import { getErrorMessage } from "~/features/common/helpers"; -import { useLazyDownloadReportQuery } from "~/features/consent-reporting/consent-reporting.slice"; - -const ConsentReporting = () => { - const [startDate, setStartDate] = useState(""); - const [endDate, setEndDate] = useState(""); - - const toast = useToast(); - - const [downloadReportTrigger, { isLoading }] = useLazyDownloadReportQuery(); - - const handleDownloadClicked = async () => { - const result = await downloadReportTrigger({ startDate, endDate }); - if (result.isError) { - const message = getErrorMessage( - result.error, - "A problem occurred while generating your consent report. Please try again.", - ); - toast({ status: "error", description: message }); - } else { - const a = document.createElement("a"); - const csvBlob = new Blob([result.data], { type: "text/csv" }); - a.href = window.URL.createObjectURL(csvBlob); - a.download = `consent-reports.csv`; - a.click(); - } - }; - - return ( - - - From - setStartDate(e.target.value)} - borderRadius="md" - data-testid="input-from-date" - /> - - - To - setEndDate(e.target.value)} - borderRadius="md" - data-testid="input-to-date" - /> - - - - ); -}; - -export default ConsentReporting; diff --git a/clients/admin-ui/src/features/consent-reporting/ConsentTcfDetailModal.tsx b/clients/admin-ui/src/features/consent-reporting/ConsentTcfDetailModal.tsx new file mode 100644 index 0000000000..6b0f55b9bd --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/ConsentTcfDetailModal.tsx @@ -0,0 +1,73 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { + AntEmpty as Empty, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, +} from "fidesui"; + +import { PreferencesSaved } from "~/types/api"; + +import { FidesTableV2 } from "../common/table/v2"; +import useTcfConsentColumns, { + TcfDetailRow, +} from "./hooks/useTcfConsentColumns"; + +interface ConsentTcfDetailModalProps { + isOpen: boolean; + onClose: () => void; + tcfPreferences?: PreferencesSaved; +} + +const ConsentTcfDetailModal = ({ + isOpen, + onClose, + tcfPreferences, +}: ConsentTcfDetailModalProps) => { + const { tcfColumns, mapTcfPreferencesToRowColumns } = useTcfConsentColumns(); + const tcfData = mapTcfPreferencesToRowColumns(tcfPreferences); + + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + data: tcfData || [], + columns: tcfColumns, + getRowId: (row) => `${row.key}-${row.id}`, + manualPagination: true, + }); + + return ( + + + + + TCF Consent Details + +
+ + tableInstance={tableInstance} + emptyTableNotice={ + + } + /> +
+
+
+
+ ); +}; +export default ConsentTcfDetailModal; diff --git a/clients/admin-ui/src/features/consent-reporting/consent-reporting.slice.ts b/clients/admin-ui/src/features/consent-reporting/consent-reporting.slice.ts index 61aeb6c4c5..04bf101b9c 100644 --- a/clients/admin-ui/src/features/consent-reporting/consent-reporting.slice.ts +++ b/clients/admin-ui/src/features/consent-reporting/consent-reporting.slice.ts @@ -1,38 +1,42 @@ -import { baseApi } from "~/features/common/api.slice"; - -type DateRange = { - startDate?: string; - endDate?: string; -}; +import { Dayjs } from "dayjs"; -export function convertDateRangeToSearchParams({ - startDate, - endDate, -}: DateRange) { - let startDateISO; - if (startDate) { - startDateISO = new Date(startDate); - startDateISO.setUTCHours(0, 0, 0); - } +import { baseApi } from "~/features/common/api.slice"; +import { + Page_ConsentReportingSchema_, + PreferencesSavedExtended, +} from "~/types/api"; +import { DateRange } from "~/types/common/DateRange"; +import { PaginationQueryParams } from "~/types/common/PaginationQueryParams"; - let endDateISO; - if (endDate) { - endDateISO = new Date(endDate); - endDateISO.setUTCHours(0, 0, 0); - } +const startOfDayIso = (date?: Dayjs | null) => + date?.utc()?.startOf("day").toISOString(); - return { - ...(startDateISO ? { created_gt: startDateISO.toISOString() } : {}), - ...(endDateISO ? { created_lt: endDateISO.toISOString() } : {}), - }; -} +const endOfDayIso = (date?: Dayjs | null) => + date?.utc()?.endOf("day").toISOString(); export const consentReportingApi = baseApi.injectEndpoints({ endpoints: (build) => ({ + getCurrentPrivacyPreferences: build.query< + PreferencesSavedExtended, + { search: string } + >({ + query: ({ search }) => ({ + url: "current-privacy-preferences", + params: { + email: search, + phone_number: search, + fides_user_device_id: search, + external_id: search, + }, + }), + providesTags: ["Current Privacy Preferences"], + }), + downloadReport: build.query({ query: ({ startDate, endDate }) => { const params = { - ...convertDateRangeToSearchParams({ startDate, endDate }), + created_gt: startOfDayIso(startDate), + created_lt: endOfDayIso(endDate), download_csv: "true", }; return { @@ -41,9 +45,30 @@ export const consentReportingApi = baseApi.injectEndpoints({ responseHandler: "content-type", }; }, + providesTags: ["Consent Reporting Export"], + }), + getAllHistoricalPrivacyPreferences: build.query< + Page_ConsentReportingSchema_, + PaginationQueryParams & DateRange + >({ + query: ({ page, size, startDate, endDate }) => { + return { + url: "historical-privacy-preferences", + params: { + page, + size, + request_timestamp_gt: startOfDayIso(startDate), + request_timestamp_lt: endOfDayIso(endDate), + }, + }; + }, providesTags: ["Consent Reporting"], }), }), }); -export const { useLazyDownloadReportQuery } = consentReportingApi; +export const { + useLazyDownloadReportQuery, + useGetAllHistoricalPrivacyPreferencesQuery, + useLazyGetCurrentPrivacyPreferencesQuery, +} = consentReportingApi; diff --git a/clients/admin-ui/src/features/consent-reporting/constants.ts b/clients/admin-ui/src/features/consent-reporting/constants.ts new file mode 100644 index 0000000000..5c90d2955d --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/constants.ts @@ -0,0 +1,46 @@ +import { + ConsentMethod, + RequestOrigin, + UserConsentPreference, +} from "~/types/api"; + +export const USER_CONSENT_PREFERENCE_LABELS: Record< + UserConsentPreference, + string +> = { + [UserConsentPreference.OPT_IN]: "Opt in", + [UserConsentPreference.OPT_OUT]: "Opt out", + [UserConsentPreference.ACKNOWLEDGE]: "Acknowledge", + [UserConsentPreference.TCF]: "TCF", +}; + +export const USER_CONSENT_PREFERENCE_COLOR: Record< + UserConsentPreference, + string +> = { + [UserConsentPreference.OPT_IN]: "success", + [UserConsentPreference.OPT_OUT]: "error", + [UserConsentPreference.ACKNOWLEDGE]: "default", + [UserConsentPreference.TCF]: "sandstone", +}; + +export const CONSENT_METHOD_LABELS: Record = { + [ConsentMethod.ACCEPT]: "Accept", + [ConsentMethod.BUTTON]: "Button", + [ConsentMethod.DISMISS]: "Dismiss", + [ConsentMethod.GPC]: "GPC", + [ConsentMethod.REJECT]: "Reject", + [ConsentMethod.SAVE]: "Save", + [ConsentMethod.SCRIPT]: "Script", + [ConsentMethod.INDIVIDUAL_NOTICE]: "Individual Notice", + [ConsentMethod.ACKNOWLEDGE]: "Acknowledge", +}; + +export const REQUEST_ORIGIN_LABELS: Record = { + [RequestOrigin.API]: "API", + [RequestOrigin.BANNER_AND_MODAL]: "Banner and Modal", + [RequestOrigin.MODAL]: "Modal", + [RequestOrigin.OVERLAY]: "Overlay", + [RequestOrigin.PRIVACY_CENTER]: "Privacy Center", + [RequestOrigin.TCF_OVERLAY]: "TCF Overlay", +}; diff --git a/clients/admin-ui/src/features/consent-reporting/hooks/useConsentLookupTableColumns.tsx b/clients/admin-ui/src/features/consent-reporting/hooks/useConsentLookupTableColumns.tsx new file mode 100644 index 0000000000..a9b433b4a2 --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/hooks/useConsentLookupTableColumns.tsx @@ -0,0 +1,64 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; + +import { + BadgeCell, + DefaultCell, + DefaultHeaderCell, +} from "~/features/common/table/v2"; +import { PreferenceWithNoticeInformation } from "~/types/api"; + +import { + USER_CONSENT_PREFERENCE_COLOR, + USER_CONSENT_PREFERENCE_LABELS, +} from "../constants"; + +const columnHelper = createColumnHelper(); + +const useConsentLookupTableColumns = () => { + const columns = useMemo( + () => [ + columnHelper.accessor((row) => row.notice_id, { + id: "notice_id", + cell: ({ getValue }) => , + header: (props) => , + enableSorting: false, + }), + columnHelper.accessor((row) => row.notice_key, { + id: "notice_key", + cell: ({ getValue }) => , + header: (props) => , + enableSorting: false, + }), + columnHelper.accessor((row) => row.preference, { + id: "preference", + cell: ({ getValue }) => { + const preference = getValue(); + const preferenceLabel = + (preference && USER_CONSENT_PREFERENCE_LABELS[preference]) || + preference; + + const badgeColor = + (preference && USER_CONSENT_PREFERENCE_COLOR[preference]) || ""; + + return ; + }, + header: (props) => , + enableSorting: false, + size: 100, + }), + columnHelper.accessor((row) => row.privacy_notice_history_id, { + id: "privacy_notice_history_id", + cell: ({ getValue }) => , + header: (props) => ( + + ), + enableSorting: false, + }), + ], + [], + ); + + return columns; +}; +export default useConsentLookupTableColumns; diff --git a/clients/admin-ui/src/features/consent-reporting/hooks/useConsentReportingDownload.ts b/clients/admin-ui/src/features/consent-reporting/hooks/useConsentReportingDownload.ts new file mode 100644 index 0000000000..0501e4d38c --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/hooks/useConsentReportingDownload.ts @@ -0,0 +1,38 @@ +import { Dayjs } from "dayjs"; +import { useToast } from "fidesui"; + +import { getErrorMessage } from "~/features/common/helpers"; + +import { useLazyDownloadReportQuery } from "../consent-reporting.slice"; + +const useConsentReportingDownload = () => { + const toast = useToast(); + + const [downloadReportTrigger, { isFetching }] = useLazyDownloadReportQuery(); + + const downloadReport = async ({ + startDate, + endDate, + }: { + startDate?: Dayjs | null; + endDate?: Dayjs | null; + }) => { + const result = await downloadReportTrigger({ startDate, endDate }); + if (result.isError) { + const message = getErrorMessage( + result.error, + "A problem occurred while generating your consent report. Please try again.", + ); + toast({ status: "error", description: message }); + } else { + const a = document.createElement("a"); + const csvBlob = new Blob([result.data], { type: "text/csv" }); + a.href = window.URL.createObjectURL(csvBlob); + a.download = `consent-reports.csv`; + a.click(); + } + }; + + return { downloadReport, isDownloadingReport: isFetching }; +}; +export default useConsentReportingDownload; diff --git a/clients/admin-ui/src/features/consent-reporting/hooks/useConsentReportingTableColumns.tsx b/clients/admin-ui/src/features/consent-reporting/hooks/useConsentReportingTableColumns.tsx new file mode 100644 index 0000000000..0698d9381b --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/hooks/useConsentReportingTableColumns.tsx @@ -0,0 +1,163 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { AntFlex as Flex, AntTypography as Typography } from "fidesui"; +import { useMemo } from "react"; + +import { PRIVACY_NOTICE_REGION_RECORD } from "~/features/common/privacy-notice-regions"; +import { + BadgeCell, + DefaultCell, + DefaultHeaderCell, +} from "~/features/common/table/v2"; +import { RelativeTimestampCell } from "~/features/common/table/v2/cells"; +import { + ConsentReportingSchema, + PrivacyNoticeRegion, + UserConsentPreference, +} from "~/types/api"; + +import { + CONSENT_METHOD_LABELS, + REQUEST_ORIGIN_LABELS, + USER_CONSENT_PREFERENCE_COLOR, + USER_CONSENT_PREFERENCE_LABELS, +} from "../constants"; + +const columnHelper = createColumnHelper(); + +const useConsentReportingTableColumns = ({ + onTcfDetailViewClick, +}: { + onTcfDetailViewClick: (preferences: UserConsentPreference) => void; +}) => { + const columns = useMemo( + () => [ + columnHelper.accessor((row) => row.fides_user_device_id, { + id: "fides_user_device_id", + cell: ({ getValue }) => , + header: (props) => ( + + ), + enableSorting: false, + }), + columnHelper.accessor((row) => row.user_geography, { + id: "user_geography", + cell: ({ getValue }) => { + const region = getValue() as PrivacyNoticeRegion | null | undefined; + const regionLabel = + (region && PRIVACY_NOTICE_REGION_RECORD[region]) || region; + return ; + }, + header: (props) => ( + + ), + enableSorting: false, + }), + columnHelper.accessor((row) => row.preference, { + id: "preference", + cell: ({ getValue, row }) => { + const preference = getValue(); + const preferenceLabel = + (preference && USER_CONSENT_PREFERENCE_LABELS[preference]) || + preference; + + const badgeColor = + (preference && USER_CONSENT_PREFERENCE_COLOR[preference]) || ""; + + const hasTcfDetails = + preference === "tcf" && row.original.tcf_preferences; + + return ( + + onTcfDetailViewClick(row.original.tcf_preferences!) + : undefined + } + /> + + ); + }, + header: (props) => , + enableSorting: false, + size: 100, + }), + columnHelper.accessor((row) => row.notice_name, { + id: "notice_name", + cell: ({ getValue }) => { + const value = getValue(); + const label = value === "tcf" ? value.toUpperCase() : value; + return ; + }, + header: (props) => ( + + ), + enableSorting: false, + }), + columnHelper.accessor((row) => row.method, { + id: "method", + cell: ({ getValue }) => { + const method = getValue(); + const methodLabel = + (method && CONSENT_METHOD_LABELS[method]) || method; + return ; + }, + header: (props) => , + enableSorting: false, + size: 100, + }), + columnHelper.accessor((row) => row.request_origin, { + id: "request_origin", + cell: ({ getValue }) => { + const requestOrigin = getValue(); + const requestOriginLabel = + (requestOrigin && REQUEST_ORIGIN_LABELS[requestOrigin]) || + requestOrigin; + + return ; + }, + header: (props) => ( + + ), + enableSorting: false, + size: 120, + }), + columnHelper.accessor((row) => row.request_timestamp, { + id: "request_timestamp", + cell: ({ getValue }) => , + header: (props) => ( + + ), + size: 120, + }), + columnHelper.accessor((row) => row.email, { + id: "email", + cell: ({ getValue }) => ( + + {getValue()} + + } + /> + ), + header: (props) => , + enableSorting: false, + }), + columnHelper.accessor((row) => row.id, { + id: "id", + cell: ({ getValue }) => , + header: (props) => ( + + ), + enableSorting: false, + }), + ], + [onTcfDetailViewClick], + ); + + return columns; +}; +export default useConsentReportingTableColumns; diff --git a/clients/admin-ui/src/features/consent-reporting/hooks/useTcfConsentColumns.tsx b/clients/admin-ui/src/features/consent-reporting/hooks/useTcfConsentColumns.tsx new file mode 100644 index 0000000000..9dc87ddc7e --- /dev/null +++ b/clients/admin-ui/src/features/consent-reporting/hooks/useTcfConsentColumns.tsx @@ -0,0 +1,85 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { forEach } from "lodash"; +import { useCallback, useMemo } from "react"; + +import { + BadgeCell, + DefaultCell, + DefaultHeaderCell, +} from "~/features/common/table/v2"; +import { PreferencesSaved, UserConsentPreference } from "~/types/api"; + +import { + USER_CONSENT_PREFERENCE_COLOR, + USER_CONSENT_PREFERENCE_LABELS, +} from "../constants"; + +export interface TcfDetailRow { + key: string; + id: string | number; + preference: UserConsentPreference; +} + +const columnHelper = createColumnHelper(); + +const useTcfConsentColumns = () => { + const mapTcfPreferencesToRowColumns = useCallback( + (allPreferences?: PreferencesSaved | null) => { + const results: TcfDetailRow[] = []; + if (allPreferences) { + const { preferences, ...tcfPreferences } = allPreferences; + forEach(tcfPreferences, (records, key) => { + if (records) { + records.forEach((value) => { + results.push({ + key, + id: value.id, + preference: value.preference, + }); + }); + } + }); + } + return results; + }, + [], + ); + + const columns = useMemo( + () => [ + columnHelper.accessor((row) => row.key, { + id: "key", + cell: ({ getValue }) => , + header: (props) => , + enableSorting: false, + }), + columnHelper.accessor((row) => row.id, { + id: "id", + cell: ({ getValue }) => , + header: (props) => , + enableSorting: false, + }), + columnHelper.accessor((row) => row.preference, { + id: "preference", + cell: ({ getValue }) => { + const preference = getValue(); + const preferenceLabel = + (preference && USER_CONSENT_PREFERENCE_LABELS[preference]) || + preference; + + const badgeColor = + (preference && USER_CONSENT_PREFERENCE_COLOR[preference]) || ""; + + return ; + }, + header: (props) => , + enableSorting: false, + size: 100, + }), + ], + [], + ); + + return { tcfColumns: columns, mapTcfPreferencesToRowColumns }; +}; +export default useTcfConsentColumns; diff --git a/clients/admin-ui/src/pages/_app.tsx b/clients/admin-ui/src/pages/_app.tsx index 3493642c9d..e782716e96 100644 --- a/clients/admin-ui/src/pages/_app.tsx +++ b/clients/admin-ui/src/pages/_app.tsx @@ -5,6 +5,8 @@ import "@fontsource/inter/700.css"; import "../theme/tailwind.css"; import "../theme/global.scss"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; import { FidesUIProvider, Flex } from "fidesui"; import type { AppProps } from "next/app"; import React, { ReactNode } from "react"; @@ -23,6 +25,8 @@ import theme from "../theme"; import Login from "./login"; import LoginWithOIDC from "./login/[provider]"; +dayjs.extend(utc); + if (process.env.NEXT_PUBLIC_MOCK_API) { // eslint-disable-next-line global-require require("../mocks"); diff --git a/clients/admin-ui/src/pages/consent/reporting/index.tsx b/clients/admin-ui/src/pages/consent/reporting/index.tsx index 49b0002220..a341f41d71 100644 --- a/clients/admin-ui/src/pages/consent/reporting/index.tsx +++ b/clients/admin-ui/src/pages/consent/reporting/index.tsx @@ -1,24 +1,197 @@ -import { Box, Text } from "fidesui"; -import React from "react"; +/* eslint-disable react/no-unstable-nested-components */ +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import dayjs, { Dayjs } from "dayjs"; +import { + AntButton as Button, + AntDateRangePicker as DateRangePicker, + AntDropdown as Dropdown, + AntEmpty as Empty, + AntFlex as Flex, + Icons, + useToast, +} from "fidesui"; +import React, { useMemo, useState } from "react"; -import Layout from "~/features/common/Layout"; +import FixedLayout from "~/features/common/FixedLayout"; import PageHeader from "~/features/common/PageHeader"; -import ConsentReporting from "~/features/consent-reporting/ConsentReporting"; - -const ConsentReportingPage = () => ( - - - - - Download a CSV containing a report of consent preferences made by users - on your sites. Select a date range below and click "Download - report". Depending on the number of records in the date range you - select, it may take several minutes to prepare the file after you click - "Download report". - - - - -); +import { + FidesTableV2, + PAGE_SIZES, + PaginationBar, + TableActionBar, + TableSkeletonLoader, + useServerSidePagination, +} from "~/features/common/table/v2"; +import { successToastParams } from "~/features/common/toast"; +import { useGetAllHistoricalPrivacyPreferencesQuery } from "~/features/consent-reporting/consent-reporting.slice"; +import ConsentLookupModal from "~/features/consent-reporting/ConsentLookupModal"; +import ConsentReportDownloadModal from "~/features/consent-reporting/ConsentReportDownloadModal"; +import ConsentTcfDetailModal from "~/features/consent-reporting/ConsentTcfDetailModal"; +import useConsentReportingTableColumns from "~/features/consent-reporting/hooks/useConsentReportingTableColumns"; +import { ConsentReportingSchema } from "~/types/api"; + +const ConsentReportingPage = () => { + const pagination = useServerSidePagination(); + const today = useMemo(() => dayjs(), []); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [isConsentLookupModalOpen, setIsConsentLookupModalOpen] = + useState(false); + const [isDownloadReportModalOpen, setIsDownloadReportModalOpen] = + useState(false); + const [isConsentTcfDetailModalOpen, setIsConsentTcfDetailModalOpen] = + useState(false); + const [currentTcfPreferences, setCurrentTcfPreferences] = useState(); + + const toast = useToast(); + + const { data, isLoading, isFetching, refetch } = + useGetAllHistoricalPrivacyPreferencesQuery({ + page: pagination.pageIndex, + size: pagination.pageSize, + startDate, + endDate, + }); + + const { setTotalPages } = pagination; + const { items: privacyPreferences, total: totalRows } = useMemo(() => { + const results = data || { items: [], total: 0, pages: 0 }; + setTotalPages(results.pages); + return results; + }, [data, setTotalPages]); + + const onTcfDetailViewClick = (tcfPreferences: any) => { + setIsConsentTcfDetailModalOpen(true); + setCurrentTcfPreferences(tcfPreferences); + }; + + const columns = useConsentReportingTableColumns({ onTcfDetailViewClick }); + const tableInstance = useReactTable({ + getCoreRowModel: getCoreRowModel(), + data: privacyPreferences, + columns, + getRowId: (row) => `${row.id}`, + manualPagination: true, + }); + + const handleClickRefresh = async () => { + pagination.resetPageIndexToDefault(); + await refetch(); + toast( + successToastParams( + "Consent report refreshed successfully.", + "Report Refreshed", + ), + ); + }; + + return ( + + + Refresh + + } + /> +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + + { + setStartDate(dates && dates[0]); + setEndDate(dates && dates[1]); + }} + /> + +
+ setIsConsentLookupModalOpen(false)} + /> + setIsDownloadReportModalOpen(false)} + startDate={startDate} + endDate={endDate} + /> + { + setIsConsentTcfDetailModalOpen(false); + setCurrentTcfPreferences(undefined); + }} + tcfPreferences={currentTcfPreferences} + /> +
+ ); +}; export default ConsentReportingPage; diff --git a/clients/admin-ui/src/theme/global.scss b/clients/admin-ui/src/theme/global.scss index 22ccf01d2e..bf67df62d3 100644 --- a/clients/admin-ui/src/theme/global.scss +++ b/clients/admin-ui/src/theme/global.scss @@ -37,6 +37,11 @@ h6 { --ant-button-default-bg: var(--fidesui-neutral-50); } +// Input labels should have a font weight of 600 +.ant-form-item-label { + font-weight: 600; +} + // Custom styles for dark submenus .ant-menu-dark .ant-menu-sub { // The unselected submenu item should be light gray diff --git a/clients/admin-ui/src/types/common/DateRange.ts b/clients/admin-ui/src/types/common/DateRange.ts new file mode 100644 index 0000000000..6fc8129928 --- /dev/null +++ b/clients/admin-ui/src/types/common/DateRange.ts @@ -0,0 +1,6 @@ +import { Dayjs } from "dayjs"; + +export type DateRange = { + startDate?: Dayjs | null; + endDate?: Dayjs | null; +}; diff --git a/clients/fidesui/src/hoc/CustomDateRangePicker.tsx b/clients/fidesui/src/hoc/CustomDateRangePicker.tsx new file mode 100644 index 0000000000..365a7833e3 --- /dev/null +++ b/clients/fidesui/src/hoc/CustomDateRangePicker.tsx @@ -0,0 +1,31 @@ +import { ArrowRight, Calendar } from "@carbon/icons-react"; +import { RangePickerProps } from "antd/es/date-picker"; +import { DatePicker } from "antd/lib"; +import React from "react"; + +const withCustomProps = (WrappedComponent: typeof DatePicker.RangePicker) => { + const WrappedSelect = ({ + suffixIcon = , + separator = ( + + ), + ...props + }: RangePickerProps) => { + const customProps = { + suffixIcon, + separator, + ...props, + }; + return ; + }; + return WrappedSelect; +}; +/** + * Higher-order component that adds consistent styling and enhanced functionality to Ant Design's RangePicker component. + * + * Default customizations: + * - Uses Carbon icons for suffix (calendar icon) and separator (right arrow) + * - Uses the same gray color for the right arrow icon + * + */ +export const CustomDateRangePicker = withCustomProps(DatePicker.RangePicker); diff --git a/clients/fidesui/src/hoc/index.tsx b/clients/fidesui/src/hoc/index.tsx index 23ef42ac22..f114668e23 100644 --- a/clients/fidesui/src/hoc/index.tsx +++ b/clients/fidesui/src/hoc/index.tsx @@ -1,2 +1,3 @@ +export * from "./CustomDateRangePicker"; export * from "./CustomSelect"; export * from "./CustomTag"; diff --git a/clients/fidesui/src/index.ts b/clients/fidesui/src/index.ts index 377e486428..ba2e6db1e7 100644 --- a/clients/fidesui/src/index.ts +++ b/clients/fidesui/src/index.ts @@ -61,6 +61,7 @@ export type { // Higher-order components export { CustomSelect as AntSelect } from "./hoc"; +export { CustomDateRangePicker as AntDateRangePicker } from "./hoc"; /** * Custom Re-exports diff --git a/clients/package-lock.json b/clients/package-lock.json index 0192e62380..23c90679e4 100644 --- a/clients/package-lock.json +++ b/clients/package-lock.json @@ -29,6 +29,7 @@ "d3-hierarchy": "^3.1.2", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", + "dayjs": "^1.11.13", "fides-js": "^0.0.1", "fidesui": "*", "file-saver": "^2.0.5", @@ -10886,9 +10887,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { "version": "4.3.5",