From 3254c6ddec706c93570d4ab3e42afff4b022823a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 4 May 2026 19:11:29 +0400 Subject: [PATCH] Add cookie banner detection page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Displays uncategorised cookie patterns in a sortable, filterable table under a new Detection tab on the banner configuration layout. Signed-off-by: Émile Ré --- .../CookieBannerConfigLayout.tsx | 6 +- .../detection/CookieBannerDetectionPage.tsx | 205 ++++++++++++++++++ .../CookieBannerDetectionPageLoader.tsx | 47 ++++ .../CookieBannerDetectionPageSkeleton.tsx | 30 +++ .../_components/DetectionPatternRow.tsx | 74 +++++++ .../organizations/cookie-banners/routes.ts | 5 + 6 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPage.tsx create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageSkeleton.tsx create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/detection/_components/DetectionPatternRow.tsx diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/CookieBannerConfigLayout.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/CookieBannerConfigLayout.tsx index 5a3edadb7..3e74bca6f 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/CookieBannerConfigLayout.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/CookieBannerConfigLayout.tsx @@ -12,7 +12,7 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -import { ClipboardTextIcon } from "@phosphor-icons/react"; +import { ClipboardTextIcon, MagnifyingGlassIcon } from "@phosphor-icons/react"; import { formatError, type GraphQLError } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { @@ -252,6 +252,10 @@ export default function CookieBannerConfigLayout({ queryRef }: CookieBannerConfi {__("Cookies")} + + + {__("Detection")} + {__("Consent Records")} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPage.tsx new file mode 100644 index 000000000..e34ddca96 --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPage.tsx @@ -0,0 +1,205 @@ +// 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 { useTranslate } from "@probo/i18n"; +import { + Card, + Input, + Option, + Select, + Tbody, + Thead, + Tr, +} from "@probo/ui"; +import { type ComponentProps, useState, useTransition } from "react"; +import { + graphql, + type PreloadedQuery, + usePaginationFragment, + usePreloadedQuery, +} from "react-relay"; + +import type { CookieBannerDetectionPageFragment$key } from "#/__generated__/core/CookieBannerDetectionPageFragment.graphql"; +import type { CookieBannerDetectionPageQuery } from "#/__generated__/core/CookieBannerDetectionPageQuery.graphql"; +import type { + CookieBannerDetectionPageRefetchQuery, + CookiePatternOrderField, + CookieSource, +} from "#/__generated__/core/CookieBannerDetectionPageRefetchQuery.graphql"; +import { SortableTable, SortableTh } from "#/components/SortableTable"; + +import { DetectionPatternRow } from "./_components/DetectionPatternRow"; + +export const cookieBannerDetectionPageQuery = graphql` + query CookieBannerDetectionPageQuery($cookieBannerId: ID!) { + node(id: $cookieBannerId) @required(action: THROW) { + __typename + ... on CookieBanner { + ...CookieBannerDetectionPageFragment + } + } + } +`; + +const detectionFragment = graphql` + fragment CookieBannerDetectionPageFragment on CookieBanner + @refetchable(queryName: "CookieBannerDetectionPageRefetchQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 50 } + order: { type: "CookiePatternOrder", defaultValue: { field: NAME, direction: ASC } } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + query: { type: "String", defaultValue: null } + source: { type: "CookieSource", defaultValue: null } + ) { + uncategorisedPatterns( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + filter: { query: $query, source: $source } + ) + @connection( + key: "CookieBannerDetectionPage_uncategorisedPatterns" + filters: ["filter", "orderBy"] + ) + @required(action: THROW) { + edges { + node { + id + ...DetectionPatternRowFragment + } + } + } + } +`; + +interface CookieBannerDetectionPageProps { + queryRef: PreloadedQuery; +} + +export default function CookieBannerDetectionPage({ + queryRef, +}: CookieBannerDetectionPageProps) { + const { __ } = useTranslate(); + const data = usePreloadedQuery(cookieBannerDetectionPageQuery, queryRef); + + if (data.node.__typename !== "CookieBanner") { + throw new Error("invalid type for node"); + } + + const [isPending, startTransition] = useTransition(); + const [queryFilter, setQueryFilter] = useState(""); + const [sourceFilter, setSourceFilter] = useState(null); + + const { data: fragmentData, ...pagination } = usePaginationFragment< + CookieBannerDetectionPageRefetchQuery, + CookieBannerDetectionPageFragment$key + >(detectionFragment, data.node); + + const patterns = fragmentData.uncategorisedPatterns.edges.map(edge => edge.node) ?? []; + + const refetchFilters = (overrides: Record = {}) => { + startTransition(() => { + pagination.refetch( + { + query: queryFilter || null, + source: sourceFilter, + ...overrides, + }, + { fetchPolicy: "network-only" }, + ); + }); + }; + + const handleQuerySubmit = () => { + refetchFilters({ query: queryFilter || null }); + }; + + const handleSourceFilterChange = (value: string) => { + const newSource = value === "ALL" ? null : (value as CookieSource); + setSourceFilter(newSource); + refetchFilters({ source: newSource }); + }; + + const refetchWithFilters: ComponentProps["refetch"] = ({ order }) => { + pagination.refetch({ + order: { direction: order.direction, field: order.field as CookiePatternOrderField }, + query: queryFilter || null, + source: sourceFilter, + }); + }; + + return ( +
+
+ setQueryFilter(e.target.value)} + onKeyDown={e => e.key === "Enter" && handleQuerySubmit()} + onBlur={handleQuerySubmit} + className="w-72" + /> + +
+ +
+ {patterns.length > 0 + ? ( + + + + {__("Name")} + {__("Source")} + {__("Last Matched")} + {__("Updated")} + + + + {patterns.map(pattern => ( + + ))} + + + ) + : ( + +
+

+ {__("No uncategorised patterns")} +

+

+ {__("All detected cookie patterns have been categorised. New patterns will appear here when detected.")} +

+
+
+ )} +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageLoader.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageLoader.tsx new file mode 100644 index 000000000..e659577fa --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageLoader.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 { CookieBannerDetectionPageQuery } from "#/__generated__/core/CookieBannerDetectionPageQuery.graphql"; + +import CookieBannerDetectionPage, { cookieBannerDetectionPageQuery } from "./CookieBannerDetectionPage"; +import { CookieBannerDetectionPageSkeleton } from "./CookieBannerDetectionPageSkeleton"; + +export default function CookieBannerDetectionPageLoader() { + const { cookieBannerId } = useParams<{ cookieBannerId: string }>(); + if (typeof cookieBannerId !== "string") { + throw new Error("Missing cookieBannerId parameter"); + } + + const [queryRef, loadQuery] = useQueryLoader( + cookieBannerDetectionPageQuery, + ); + + useEffect(() => { + loadQuery({ cookieBannerId }); + }, [loadQuery, cookieBannerId]); + + if (!queryRef) { + return ; + } + + return ( + }> + + + ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageSkeleton.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageSkeleton.tsx new file mode 100644 index 000000000..ea54f997e --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageSkeleton.tsx @@ -0,0 +1,30 @@ +// 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 CookieBannerDetectionPageSkeleton() { + return ( +
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/detection/_components/DetectionPatternRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/_components/DetectionPatternRow.tsx new file mode 100644 index 000000000..22de898a4 --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/detection/_components/DetectionPatternRow.tsx @@ -0,0 +1,74 @@ +// 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, Td, Tr } from "@probo/ui"; +import { graphql, useFragment } from "react-relay"; + +import type { DetectionPatternRowFragment$key } from "#/__generated__/core/DetectionPatternRowFragment.graphql"; + +const detectionPatternFragment = graphql` + fragment DetectionPatternRowFragment on CookiePattern { + displayName + matchType + source + description + lastMatchedAt + updatedAt + } +`; + +interface DetectionPatternRowProps { + patternKey: DetectionPatternRowFragment$key; +} + +export function DetectionPatternRow({ patternKey }: DetectionPatternRowProps) { + const { __ } = useTranslate(); + const pattern = useFragment(detectionPatternFragment, patternKey); + + return ( + + +
+ {pattern.displayName} + {pattern.description && ( + + {pattern.description} + + )} +
+ + + + {pattern.source === "SCRIPT" ? __("Script") : __("Pre-existing")} + + + + {pattern.lastMatchedAt + ? ( + + ) + : -} + + + + + + ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/routes.ts b/apps/console/src/pages/organizations/cookie-banners/routes.ts index 05a52eb9a..792e1c7ac 100644 --- a/apps/console/src/pages/organizations/cookie-banners/routes.ts +++ b/apps/console/src/pages/organizations/cookie-banners/routes.ts @@ -69,6 +69,11 @@ export const cookieBannerRoutes = [ Fallback: LinkCardSkeleton, Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordsPageLoader")), }, + { + path: "detection", + Fallback: LinkCardSkeleton, + Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/detection/CookieBannerDetectionPageLoader")), + }, ], }, {