From 9b83e319a0c7ee4609c9d20a07b195ce58df1802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 27 Apr 2026 16:01:16 +0400 Subject: [PATCH] Add consent record detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display record attributes and parsed consent data with per-category consent state and cookies from the banner version snapshot. The page lives outside the config layout with its own breadcrumb navigation. Signed-off-by: Émile Ré --- .../CookieBannerConsentRecordPage.tsx | 244 ++++++++++++++++++ .../CookieBannerConsentRecordPageLoader.tsx | 47 ++++ .../CookieBannerConsentRecordPageSkeleton.tsx | 43 +++ .../CookieBannerConsentRecordsPage.tsx | 1 - .../_components/ConsentRecordRow.tsx | 49 +--- .../_components/consentRecordHelpers.ts | 58 +++++ .../organizations/cookie-banners/routes.ts | 5 + pkg/cookiebanner/service.go | 28 ++ pkg/coredata/cookie_consent_record.go | 63 ++++- pkg/server/api/console/v1/base_resolvers.go | 10 + .../api/console/v1/cookie_banner_resolvers.go | 45 ++++ .../v1/cookie_consent_record_resolvers.go | 21 +- .../console/v1/graphql/cookie_banner.graphql | 15 ++ 13 files changed, 588 insertions(+), 41 deletions(-) create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageSkeleton.tsx create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/_components/consentRecordHelpers.ts diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx new file mode 100644 index 000000000..a3d01adc3 --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPage.tsx @@ -0,0 +1,244 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { formatDate } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { Badge, Breadcrumb, Card, PageHeader, PropertyRow } from "@probo/ui"; +import { useMemo } from "react"; +import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay"; + +import type { CookieBannerConsentRecordPageQuery } from "#/__generated__/core/CookieBannerConsentRecordPageQuery.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; + +import { + formatAnonymizedIp, + getActionLabel, + getActionVariant, +} from "./_components/consentRecordHelpers"; + +export const cookieBannerConsentRecordPageQuery = graphql` + query CookieBannerConsentRecordPageQuery($consentRecordId: ID!) { + node(id: $consentRecordId) @required(action: THROW) { + __typename + ... on CookieConsentRecord { + id + visitorId + action + cookieBanner @required(action: THROW) { + id + name + } + cookieBannerVersion @required(action: THROW) { + id + version + categories { + name + slug + description + kind + cookies { + name + duration + description + } + } + } + ipAddress + userAgent + sdkVersion + consentData + createdAt + } + } + } +`; + +interface CookieBannerConsentRecordPageProps { + queryRef: PreloadedQuery; +} + +export default function CookieBannerConsentRecordPage({ + queryRef, +}: CookieBannerConsentRecordPageProps) { + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + const data = usePreloadedQuery(cookieBannerConsentRecordPageQuery, queryRef); + + if (data.node.__typename !== "CookieConsentRecord") { + throw new Error("invalid type for node"); + } + + const record = data.node; + const bannerId = record.cookieBanner.id; + const bannerName = record.cookieBanner.name; + + const consentMap = useMemo(() => { + try { + return JSON.parse(record.consentData) as Record; + } catch { + return {}; + } + }, [record.consentData]); + + const categories = record.cookieBannerVersion.categories; + + return ( +
+ + + + + + + {record.visitorId} + + + + {getActionLabel(record.action, __)} + + + + {record.cookieBannerVersion + ? ( + + {record.cookieBannerVersion.version} + + ) + : ( + - + )} + + + + {record.ipAddress + ? formatAnonymizedIp(record.ipAddress) + : "-"} + + + + + {record.userAgent ?? "-"} + + + + {record.sdkVersion} + + + + + + + +

{__("Consent Data")}

+ {categories.length > 0 + ? ( +
+ {categories.map((category) => { + const consented = consentMap[category.slug]; + return ( +
+
+
+ {category.name} + {category.kind === "NECESSARY" && ( + + ( + {__("Required")} + ) + + )} +
+ + {consented ? __("Accepted") : __("Rejected")} + +
+ {category.description && ( +

+ {category.description} +

+ )} + {category.cookies.length > 0 && ( +
+ + + + + + + + + + {category.cookies.map(cookie => ( + + + + + + ))} + +
+ {__("Cookie")} + + {__("Duration")} + + {__("Description")} +
+ {cookie.name} + + {cookie.duration} + + {cookie.description} +
+
+ )} +
+ ); + })} +
+ ) + : ( +

+ {record.consentData} +

+ )} +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageLoader.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageLoader.tsx new file mode 100644 index 000000000..19a9261fd --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageLoader.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { Suspense, useEffect } from "react"; +import { useQueryLoader } from "react-relay"; +import { useParams } from "react-router"; + +import type { CookieBannerConsentRecordPageQuery } from "#/__generated__/core/CookieBannerConsentRecordPageQuery.graphql"; + +import CookieBannerConsentRecordPage, { cookieBannerConsentRecordPageQuery } from "./CookieBannerConsentRecordPage"; +import { CookieBannerConsentRecordPageSkeleton } from "./CookieBannerConsentRecordPageSkeleton"; + +export default function CookieBannerConsentRecordPageLoader() { + const { consentRecordId } = useParams<{ consentRecordId: string }>(); + if (typeof consentRecordId !== "string") { + throw new Error("Missing consentRecordId parameter"); + } + + const [queryRef, loadQuery] = useQueryLoader( + cookieBannerConsentRecordPageQuery, + ); + + useEffect(() => { + loadQuery({ consentRecordId }); + }, [loadQuery, consentRecordId]); + + if (!queryRef) { + return ; + } + + return ( + }> + + + ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageSkeleton.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageSkeleton.tsx new file mode 100644 index 000000000..a989cca70 --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageSkeleton.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +export function CookieBannerConsentRecordPageSkeleton() { + return ( +
+
+ {Array.from({ length: 7 }).map((_, i) => ( +
+
+
+
+ ))} +
+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordsPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordsPage.tsx index 6efff3dac..58d16441f 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordsPage.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordsPage.tsx @@ -211,7 +211,6 @@ export default function CookieBannerConsentRecordsPage({ {__("Banner Version")} {__("IP Address")} {__("SDK Version")} - {__("Consent Data")} {__("Date")} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/_components/ConsentRecordRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/_components/ConsentRecordRow.tsx index cdff4078d..74aaf5fb1 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/_components/ConsentRecordRow.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/consent-records/_components/ConsentRecordRow.tsx @@ -19,8 +19,15 @@ import { graphql, useFragment } from "react-relay"; import type { ConsentRecordRowFragment$key } from "#/__generated__/core/ConsentRecordRowFragment.graphql"; +import { + formatAnonymizedIp, + getActionLabel, + getActionVariant, +} from "./consentRecordHelpers"; + const consentRecordFragment = graphql` fragment ConsentRecordRowFragment on CookieConsentRecord { + id visitorId action cookieBannerVersion { @@ -29,41 +36,10 @@ const consentRecordFragment = graphql` } ipAddress sdkVersion - consentData createdAt } `; -function getActionLabel(action: string, __: (s: string) => string): string { - switch (action) { - case "ACCEPT_ALL": - return __("Accept All"); - case "REJECT_ALL": - return __("Reject All"); - case "CUSTOMIZE": - return __("Customize"); - case "GPC": - return __("GPC"); - default: - return action; - } -} - -function getActionVariant(action: string): "success" | "danger" | "warning" | "neutral" { - switch (action) { - case "ACCEPT_ALL": - return "success"; - case "REJECT_ALL": - return "danger"; - case "CUSTOMIZE": - return "warning"; - case "GPC": - return "neutral"; - default: - return "neutral"; - } -} - interface ConsentRecordRowProps { recordKey: ConsentRecordRowFragment$key; } @@ -73,7 +49,7 @@ export function ConsentRecordRow({ recordKey }: ConsentRecordRowProps) { const record = useFragment(consentRecordFragment, recordKey); return ( - + {record.visitorId} @@ -92,16 +68,13 @@ export function ConsentRecordRow({ recordKey }: ConsentRecordRowProps) { : -} - {record.ipAddress ?? "-"} + + {record.ipAddress ? formatAnonymizedIp(record.ipAddress) : "-"} + {record.sdkVersion} - - - {record.consentData} - -