Add tracker pattern detail page with properties and detected trackers sections
Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -307,7 +307,7 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="font-mono text-sm">{resource.path}</span>
|
||||
<span className="font-mono text-xs break-all max-w-xs inline-block">{resource.path}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
{resource.lastDetectedAt
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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 { Breadcrumb, PageHeader } from "@probo/ui";
|
||||
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
|
||||
import type { TrackerPatternDetailPageQuery } from "#/__generated__/core/TrackerPatternDetailPageQuery.graphql";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import { TrackerPatternDetectedTrackersSection } from "./_components/TrackerPatternDetectedTrackersSection";
|
||||
import { TrackerPatternPropertiesSection } from "./_components/TrackerPatternPropertiesSection";
|
||||
|
||||
export const trackerPatternDetailPageQuery = graphql`
|
||||
query TrackerPatternDetailPageQuery(
|
||||
$cookieBannerId: ID!
|
||||
$trackerPatternId: ID!
|
||||
) {
|
||||
cookieBanner: node(id: $cookieBannerId) @required(action: THROW) {
|
||||
__typename
|
||||
... on CookieBanner {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
node(id: $trackerPatternId) @required(action: THROW) {
|
||||
__typename
|
||||
... on TrackerPattern {
|
||||
id
|
||||
displayName
|
||||
...TrackerPatternPropertiesSection_trackerPattern
|
||||
...TrackerPatternDetectedTrackersSection_trackerPattern
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface TrackerPatternDetailPageProps {
|
||||
queryRef: PreloadedQuery<TrackerPatternDetailPageQuery>;
|
||||
}
|
||||
|
||||
export default function TrackerPatternDetailPage({
|
||||
queryRef,
|
||||
}: TrackerPatternDetailPageProps) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const data = usePreloadedQuery(trackerPatternDetailPageQuery, queryRef);
|
||||
|
||||
if (data.cookieBanner.__typename !== "CookieBanner") {
|
||||
throw new Error("invalid type for cookieBanner node");
|
||||
}
|
||||
if (data.node.__typename !== "TrackerPattern") {
|
||||
throw new Error("invalid type for node");
|
||||
}
|
||||
|
||||
const cookieBanner = data.cookieBanner;
|
||||
const pattern = data.node;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: __("Cookie Banners"),
|
||||
to: `/organizations/${organizationId}/cookie-banners`,
|
||||
},
|
||||
{
|
||||
label: cookieBanner.name,
|
||||
to: `/organizations/${organizationId}/cookie-banners/${cookieBanner.id}/settings`,
|
||||
},
|
||||
{
|
||||
label: __("Trackers"),
|
||||
to: `/organizations/${organizationId}/cookie-banners/${cookieBanner.id}/trackers`,
|
||||
},
|
||||
{
|
||||
label: pattern.displayName,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<PageHeader title={pattern.displayName} />
|
||||
|
||||
<TrackerPatternPropertiesSection trackerPatternKey={pattern} />
|
||||
|
||||
<TrackerPatternDetectedTrackersSection trackerPatternKey={pattern} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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 { TrackerPatternDetailPageQuery } from "#/__generated__/core/TrackerPatternDetailPageQuery.graphql";
|
||||
|
||||
import TrackerPatternDetailPage, { trackerPatternDetailPageQuery } from "./TrackerPatternDetailPage";
|
||||
import { TrackerPatternDetailPageSkeleton } from "./TrackerPatternDetailPageSkeleton";
|
||||
|
||||
export default function TrackerPatternDetailPageLoader() {
|
||||
const { cookieBannerId, trackerPatternId } = useParams<{
|
||||
cookieBannerId: string;
|
||||
trackerPatternId: string;
|
||||
}>();
|
||||
if (typeof cookieBannerId !== "string") {
|
||||
throw new Error("Missing cookieBannerId parameter");
|
||||
}
|
||||
if (typeof trackerPatternId !== "string") {
|
||||
throw new Error("Missing trackerPatternId parameter");
|
||||
}
|
||||
|
||||
const [queryRef, loadQuery] = useQueryLoader<TrackerPatternDetailPageQuery>(
|
||||
trackerPatternDetailPageQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ cookieBannerId, trackerPatternId });
|
||||
}, [loadQuery, cookieBannerId, trackerPatternId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <TrackerPatternDetailPageSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<TrackerPatternDetailPageSkeleton />}>
|
||||
<TrackerPatternDetailPage queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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 TrackerPatternDetailPageSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
<div className="rounded-2xl border border-border-low p-6 space-y-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center justify-between py-3 border-b border-border-low last:border-b-0">
|
||||
<div className="h-4 w-28 rounded bg-bg-subtle" />
|
||||
<div className="h-4 w-48 rounded bg-bg-subtle" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-2xl border border-border-low p-6 space-y-4">
|
||||
<div className="h-5 w-48 rounded bg-bg-subtle" />
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4 py-3 border-b border-border-low last:border-b-0">
|
||||
<div className="h-4 w-32 rounded bg-bg-subtle" />
|
||||
<div className="h-4 w-48 rounded bg-bg-subtle" />
|
||||
<div className="h-4 w-16 rounded bg-bg-subtle" />
|
||||
<div className="h-4 w-20 rounded bg-bg-subtle" />
|
||||
<div className="h-4 w-28 rounded bg-bg-subtle" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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 { Badge, Td, Tr } from "@probo/ui";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import type { DetectedTrackerRow_detectedTracker$key } from "#/__generated__/core/DetectedTrackerRow_detectedTracker.graphql";
|
||||
|
||||
const detectedTrackerFragment = graphql`
|
||||
fragment DetectedTrackerRow_detectedTracker on DetectedTracker {
|
||||
id
|
||||
identifier
|
||||
initiatorUrl
|
||||
maxAgeSeconds
|
||||
source
|
||||
lastDetectedAt
|
||||
}
|
||||
`;
|
||||
|
||||
function sourceBadge(source: string, __: (s: string) => string) {
|
||||
switch (source) {
|
||||
case "SCRIPT": return { label: __("Script"), variant: "info" as const };
|
||||
case "PRE_EXISTING": return { label: __("Pre-existing"), variant: "outline" as const };
|
||||
default: return { label: source, variant: "neutral" as const };
|
||||
}
|
||||
}
|
||||
|
||||
interface DetectedTrackerRowProps {
|
||||
detectedTrackerKey: DetectedTrackerRow_detectedTracker$key;
|
||||
}
|
||||
|
||||
export function DetectedTrackerRow({ detectedTrackerKey }: DetectedTrackerRowProps) {
|
||||
const { __ } = useTranslate();
|
||||
const tracker = useFragment(detectedTrackerFragment, detectedTrackerKey);
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>
|
||||
<span className="font-mono text-xs break-all max-w-xs inline-block">{tracker.identifier}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
{tracker.initiatorUrl
|
||||
? <span className="font-mono text-xs break-all max-w-xs inline-block">{tracker.initiatorUrl}</span>
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td>
|
||||
{tracker.maxAgeSeconds != null
|
||||
? <span className="text-sm">{tracker.maxAgeSeconds}</span>
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td>
|
||||
{tracker.source
|
||||
? (
|
||||
<Badge variant={sourceBadge(tracker.source, __).variant}>
|
||||
{sourceBadge(tracker.source, __).label}
|
||||
</Badge>
|
||||
)
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td>
|
||||
<time dateTime={tracker.lastDetectedAt}>
|
||||
{new Date(tracker.lastDetectedAt).toLocaleString()}
|
||||
</time>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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, Tbody, Th, Thead, Tr } from "@probo/ui";
|
||||
import { type ComponentProps } from "react";
|
||||
import { graphql, usePaginationFragment } from "react-relay";
|
||||
|
||||
import type { TrackerPatternDetectedTrackersSection_trackerPattern$key } from "#/__generated__/core/TrackerPatternDetectedTrackersSection_trackerPattern.graphql";
|
||||
import type {
|
||||
DetectedTrackerOrderField,
|
||||
TrackerPatternDetectedTrackersSectionRefetchQuery,
|
||||
} from "#/__generated__/core/TrackerPatternDetectedTrackersSectionRefetchQuery.graphql";
|
||||
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
||||
|
||||
import { DetectedTrackerRow } from "./DetectedTrackerRow";
|
||||
|
||||
export const trackerPatternDetectedTrackersSectionFragment = graphql`
|
||||
fragment TrackerPatternDetectedTrackersSection_trackerPattern on TrackerPattern
|
||||
@refetchable(queryName: "TrackerPatternDetectedTrackersSectionRefetchQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 50 }
|
||||
order: { type: "DetectedTrackerOrder", defaultValue: { field: LAST_DETECTED_AT, direction: DESC } }
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
) {
|
||||
detectedTrackers(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
) @connection(key: "TrackerPatternDetectedTrackersSection_detectedTrackers", filters: ["orderBy"]) {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...DetectedTrackerRow_detectedTracker
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface TrackerPatternDetectedTrackersSectionProps {
|
||||
trackerPatternKey: TrackerPatternDetectedTrackersSection_trackerPattern$key;
|
||||
}
|
||||
|
||||
export function TrackerPatternDetectedTrackersSection({
|
||||
trackerPatternKey,
|
||||
}: TrackerPatternDetectedTrackersSectionProps) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const { data, ...pagination } = usePaginationFragment<
|
||||
TrackerPatternDetectedTrackersSectionRefetchQuery,
|
||||
TrackerPatternDetectedTrackersSection_trackerPattern$key
|
||||
>(trackerPatternDetectedTrackersSectionFragment, trackerPatternKey);
|
||||
|
||||
const trackers = data.detectedTrackers?.edges.map(edge => edge.node) ?? [];
|
||||
|
||||
const refetchWithOrder: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
|
||||
pagination.refetch({
|
||||
order: { direction: order.direction, field: order.field as DetectedTrackerOrderField },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">{__("Detected Trackers")}</h3>
|
||||
|
||||
{trackers.length > 0
|
||||
? (
|
||||
<SortableTable
|
||||
{...pagination}
|
||||
refetch={refetchWithOrder}
|
||||
pageSize={50}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Identifier")}</Th>
|
||||
<SortableTh field="INITIATOR_URL">{__("Initiator URL")}</SortableTh>
|
||||
<Th>{__("Max Age (s)")}</Th>
|
||||
<Th>{__("Source")}</Th>
|
||||
<SortableTh field="LAST_DETECTED_AT">{__("Detection Time")}</SortableTh>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{trackers.map(tracker => (
|
||||
<DetectedTrackerRow
|
||||
key={tracker.id}
|
||||
detectedTrackerKey={tracker}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
)
|
||||
: (
|
||||
<Card padded>
|
||||
<div className="text-center py-12">
|
||||
<p className="text-txt-tertiary">
|
||||
{__("No detected trackers for this pattern yet.")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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 { humanizeSeconds } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Badge, Card, PropertyRow } from "@probo/ui";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import type { TrackerPatternPropertiesSection_trackerPattern$key } from "#/__generated__/core/TrackerPatternPropertiesSection_trackerPattern.graphql";
|
||||
|
||||
const trackerPatternPropertiesSectionFragment = graphql`
|
||||
fragment TrackerPatternPropertiesSection_trackerPattern on TrackerPattern {
|
||||
pattern
|
||||
matchType
|
||||
trackerType
|
||||
source
|
||||
maxAgeSeconds
|
||||
description
|
||||
excluded
|
||||
detectedCount
|
||||
lastMatchedAt
|
||||
cookieCategory {
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function trackerTypeBadge(type: string, __: (s: string) => string) {
|
||||
switch (type) {
|
||||
case "COOKIE": return { label: __("Cookie"), variant: "warning" as const };
|
||||
case "LOCAL_STORAGE": return { label: __("localStorage"), variant: "info" as const };
|
||||
case "SESSION_STORAGE": return { label: __("sessionStorage"), variant: "highlight" as const };
|
||||
case "INDEXED_DB": return { label: __("IndexedDB"), variant: "success" as const };
|
||||
case "CACHE_STORAGE": return { label: __("Cache Storage"), variant: "outline" as const };
|
||||
default: return { label: type, variant: "neutral" as const };
|
||||
}
|
||||
}
|
||||
|
||||
function sourceBadge(source: string, __: (s: string) => string) {
|
||||
switch (source) {
|
||||
case "SCRIPT": return { label: __("Script"), variant: "info" as const };
|
||||
case "PRE_EXISTING": return { label: __("Pre-existing"), variant: "outline" as const };
|
||||
default: return { label: source, variant: "neutral" as const };
|
||||
}
|
||||
}
|
||||
|
||||
interface TrackerPatternPropertiesSectionProps {
|
||||
trackerPatternKey: TrackerPatternPropertiesSection_trackerPattern$key;
|
||||
}
|
||||
|
||||
export function TrackerPatternPropertiesSection({
|
||||
trackerPatternKey,
|
||||
}: TrackerPatternPropertiesSectionProps) {
|
||||
const { __ } = useTranslate();
|
||||
const pattern = useFragment(
|
||||
trackerPatternPropertiesSectionFragment,
|
||||
trackerPatternKey,
|
||||
);
|
||||
|
||||
const typeBadge = trackerTypeBadge(pattern.trackerType, __);
|
||||
|
||||
return (
|
||||
<Card padded>
|
||||
<PropertyRow label={__("Pattern")}>
|
||||
<span className="font-mono text-sm">{pattern.pattern}</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Match Type")}>
|
||||
<span className="text-sm">{pattern.matchType === "EXACT" ? __("Exact") : __("Glob")}</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Type")}>
|
||||
<Badge variant={typeBadge.variant}>{typeBadge.label}</Badge>
|
||||
</PropertyRow>
|
||||
{pattern.source && (
|
||||
<PropertyRow label={__("Source")}>
|
||||
<Badge variant={sourceBadge(pattern.source, __).variant}>
|
||||
{sourceBadge(pattern.source, __).label}
|
||||
</Badge>
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow label={__("Category")}>
|
||||
<span className="text-sm">
|
||||
{pattern.cookieCategory?.name ?? "-"}
|
||||
</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Max Age")}>
|
||||
<span className="text-sm">
|
||||
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
|
||||
</span>
|
||||
</PropertyRow>
|
||||
{pattern.description && (
|
||||
<PropertyRow label={__("Description")}>
|
||||
<span className="text-sm">{pattern.description}</span>
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow label={__("Excluded")}>
|
||||
<span className="text-sm">{pattern.excluded ? __("Yes") : __("No")}</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Detected Count")}>
|
||||
<span className="text-sm">{pattern.detectedCount}</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Last Matched")}>
|
||||
{pattern.lastMatchedAt
|
||||
? (
|
||||
<time dateTime={pattern.lastMatchedAt}>
|
||||
{new Date(pattern.lastMatchedAt).toLocaleString()}
|
||||
</time>
|
||||
)
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</PropertyRow>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -300,9 +300,9 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
const srcBadge = pattern.source ? sourceBadge(pattern.source, __) : null;
|
||||
|
||||
return (
|
||||
<Tr className={pattern.excluded ? "bg-txt-quaternary opacity-80 line-through" : undefined}>
|
||||
<Tr to={pattern.id} className={pattern.excluded ? "bg-txt-quaternary opacity-80 line-through" : undefined}>
|
||||
<Td>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex flex-col min-w-0 max-w-xs">
|
||||
<span className={pattern.excluded ? undefined : "font-medium"}>{pattern.displayName}</span>
|
||||
{pattern.description && (
|
||||
<span className="text-xs text-txt-tertiary wrap-break-word line-clamp-1">
|
||||
|
||||
@@ -81,4 +81,9 @@ export const cookieBannerRoutes = [
|
||||
Fallback: PageSkeleton,
|
||||
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordPageLoader")),
|
||||
},
|
||||
{
|
||||
path: "cookie-banners/:cookieBannerId/trackers/:trackerPatternId",
|
||||
Fallback: PageSkeleton,
|
||||
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageLoader")),
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
|
||||
Reference in New Issue
Block a user