Show third parties on banner trackers page
Wire the new CookieBanner.linkedThirdParties aggregation and the TrackerPattern.thirdParty / commonThirdParty fields into the trackers configuration UI. Three surfaces change: CookieBannerTrackersPage: a new dedicated "Third party" column sits between Name and Source so the link is visible at a glance, and a fourth Select to the right of the category filter exposes the deduped list returned by linkedThirdParties. The Select sends the chosen GID straight through; the backend dispatches on the entity-type prefix (ThirdParty vs CommonThirdParty), so the frontend stays oblivious to which table the filter ends up hitting. The "%other" branch of the union is filtered out before render to keep TypeScript happy if the backend grows the union later. TrackerPatternRow: extends the row fragment with thirdParty and commonThirdParty, prefers the org-scoped link (mirroring the resolver priority), and renders an Avatar + name. The org-scoped ThirdParty has no logoUrl in the schema today so the Avatar falls back to initials; CommonThirdParty supplies its catalog logo. TrackerPatternPropertiesSection: same priority logic in a PropertyRow under Category, so the detail page surfaces the same information. The third-party Select uses the same set as the underlying patterns, so picking a value never produces an empty list — the linkedThirdParties resolver only walks rows the user can see. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -76,14 +76,27 @@ const trackersFragment = graphql`
|
||||
source: { type: "CookieSource", defaultValue: null }
|
||||
trackerType: { type: "TrackerType", defaultValue: null }
|
||||
cookieCategoryId: { type: "ID", defaultValue: null }
|
||||
thirdPartyId: { type: "ID", defaultValue: null }
|
||||
) {
|
||||
linkedThirdParties {
|
||||
__typename
|
||||
... on ThirdParty {
|
||||
id
|
||||
name
|
||||
}
|
||||
... on CommonThirdParty {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
trackerPatterns(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
filter: { query: $query, source: $source, trackerType: $trackerType, cookieCategoryId: $cookieCategoryId }
|
||||
filter: { query: $query, source: $source, trackerType: $trackerType, cookieCategoryId: $cookieCategoryId, thirdPartyId: $thirdPartyId }
|
||||
)
|
||||
@connection(
|
||||
key: "CookieBannerTrackersPage_trackerPatterns"
|
||||
@@ -120,6 +133,7 @@ export default function CookieBannerTrackersPage({
|
||||
const [sourceFilter, setSourceFilter] = useState<CookieSource | null>(null);
|
||||
const [trackerTypeFilter, setTrackerTypeFilter] = useState<TrackerType | null>(null);
|
||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||
const [thirdPartyFilter, setThirdPartyFilter] = useState<string | null>(null);
|
||||
|
||||
const { data: fragmentData, ...pagination } = usePaginationFragment<
|
||||
CookieBannerTrackersPageRefetchQuery,
|
||||
@@ -128,6 +142,11 @@ export default function CookieBannerTrackersPage({
|
||||
|
||||
const connectionId = fragmentData.trackerPatterns.__id;
|
||||
const patterns = fragmentData.trackerPatterns.edges.map(edge => edge.node) ?? [];
|
||||
const linkedThirdParties = (fragmentData.linkedThirdParties ?? []).filter(
|
||||
(party): party is Extract<typeof party, { id: string; name: string }> =>
|
||||
party.__typename === "ThirdParty"
|
||||
|| party.__typename === "CommonThirdParty",
|
||||
);
|
||||
|
||||
const categories = data.node.__typename === "CookieBanner"
|
||||
? data.node.categories.edges.map(edge => edge.node)
|
||||
@@ -141,6 +160,7 @@ export default function CookieBannerTrackersPage({
|
||||
source: sourceFilter,
|
||||
trackerType: trackerTypeFilter,
|
||||
cookieCategoryId: categoryFilter,
|
||||
thirdPartyId: thirdPartyFilter,
|
||||
...overrides,
|
||||
},
|
||||
{ fetchPolicy: "network-only" },
|
||||
@@ -170,6 +190,12 @@ export default function CookieBannerTrackersPage({
|
||||
refetchFilters({ cookieCategoryId: newCategory });
|
||||
};
|
||||
|
||||
const handleThirdPartyFilterChange = (value: string) => {
|
||||
const newThirdParty = value === "ALL" ? null : value;
|
||||
setThirdPartyFilter(newThirdParty);
|
||||
refetchFilters({ thirdPartyId: newThirdParty });
|
||||
};
|
||||
|
||||
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
|
||||
pagination.refetch({
|
||||
order: { direction: order.direction, field: order.field as TrackerPatternOrderField },
|
||||
@@ -177,6 +203,7 @@ export default function CookieBannerTrackersPage({
|
||||
source: sourceFilter,
|
||||
trackerType: trackerTypeFilter,
|
||||
cookieCategoryId: categoryFilter,
|
||||
thirdPartyId: thirdPartyFilter,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -220,6 +247,15 @@ export default function CookieBannerTrackersPage({
|
||||
<Option key={category.id} value={category.id}>{category.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={thirdPartyFilter ?? "ALL"}
|
||||
onValueChange={handleThirdPartyFilterChange}
|
||||
>
|
||||
<Option value="ALL">{__("All third parties")}</Option>
|
||||
{linkedThirdParties.map(party => (
|
||||
<Option key={party.id} value={party.id}>{party.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
||||
@@ -234,6 +270,7 @@ export default function CookieBannerTrackersPage({
|
||||
<Tr>
|
||||
<Th>{__("Type")}</Th>
|
||||
<SortableTh field="NAME">{__("Name")}</SortableTh>
|
||||
<Th>{__("Third party")}</Th>
|
||||
<SortableTh field="SOURCE">{__("Source")}</SortableTh>
|
||||
<Th>{__("Category")}</Th>
|
||||
<SortableTh field="LAST_MATCHED_AT">{__("Last Matched")}</SortableTh>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import { humanizeSeconds } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Badge, Card, PropertyRow } from "@probo/ui";
|
||||
import { Avatar, Badge, Card, PropertyRow } from "@probo/ui";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import type { TrackerPatternPropertiesSection_trackerPattern$key } from "#/__generated__/core/TrackerPatternPropertiesSection_trackerPattern.graphql";
|
||||
@@ -33,6 +33,15 @@ const trackerPatternPropertiesSectionFragment = graphql`
|
||||
cookieCategory {
|
||||
name
|
||||
}
|
||||
thirdParty {
|
||||
id
|
||||
name
|
||||
}
|
||||
commonThirdParty {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -93,6 +102,26 @@ export function TrackerPatternPropertiesSection({
|
||||
{pattern.cookieCategory?.name ?? "-"}
|
||||
</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Third party")}>
|
||||
{pattern.thirdParty
|
||||
? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={pattern.thirdParty.name} />
|
||||
<span className="text-sm">{pattern.thirdParty.name}</span>
|
||||
</div>
|
||||
)
|
||||
: pattern.commonThirdParty
|
||||
? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
name={pattern.commonThirdParty.name}
|
||||
src={pattern.commonThirdParty.logoUrl}
|
||||
/>
|
||||
<span className="text-sm">{pattern.commonThirdParty.name}</span>
|
||||
</div>
|
||||
)
|
||||
: <span className="text-txt-tertiary text-sm">-</span>}
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Max Age")}>
|
||||
<span className="text-sm">
|
||||
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Dropdown,
|
||||
IconArrowBoxLeft,
|
||||
@@ -56,6 +57,15 @@ const trackerPatternFragment = graphql`
|
||||
cookieCategory {
|
||||
name
|
||||
}
|
||||
thirdParty {
|
||||
id
|
||||
name
|
||||
}
|
||||
commonThirdParty {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -314,6 +324,26 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{pattern.thirdParty
|
||||
? (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Avatar name={pattern.thirdParty.name} />
|
||||
<span className="truncate">{pattern.thirdParty.name}</span>
|
||||
</div>
|
||||
)
|
||||
: pattern.commonThirdParty
|
||||
? (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Avatar
|
||||
name={pattern.commonThirdParty.name}
|
||||
src={pattern.commonThirdParty.logoUrl}
|
||||
/>
|
||||
<span className="truncate">{pattern.commonThirdParty.name}</span>
|
||||
</div>
|
||||
)
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td>
|
||||
{srcBadge
|
||||
? <Badge variant={srcBadge.variant}>{srcBadge.label}</Badge>
|
||||
|
||||
Reference in New Issue
Block a user