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 }
|
source: { type: "CookieSource", defaultValue: null }
|
||||||
trackerType: { type: "TrackerType", defaultValue: null }
|
trackerType: { type: "TrackerType", defaultValue: null }
|
||||||
cookieCategoryId: { type: "ID", defaultValue: null }
|
cookieCategoryId: { type: "ID", defaultValue: null }
|
||||||
|
thirdPartyId: { type: "ID", defaultValue: null }
|
||||||
) {
|
) {
|
||||||
|
linkedThirdParties {
|
||||||
|
__typename
|
||||||
|
... on ThirdParty {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
... on CommonThirdParty {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
trackerPatterns(
|
trackerPatterns(
|
||||||
first: $first
|
first: $first
|
||||||
after: $after
|
after: $after
|
||||||
last: $last
|
last: $last
|
||||||
before: $before
|
before: $before
|
||||||
orderBy: $order
|
orderBy: $order
|
||||||
filter: { query: $query, source: $source, trackerType: $trackerType, cookieCategoryId: $cookieCategoryId }
|
filter: { query: $query, source: $source, trackerType: $trackerType, cookieCategoryId: $cookieCategoryId, thirdPartyId: $thirdPartyId }
|
||||||
)
|
)
|
||||||
@connection(
|
@connection(
|
||||||
key: "CookieBannerTrackersPage_trackerPatterns"
|
key: "CookieBannerTrackersPage_trackerPatterns"
|
||||||
@@ -120,6 +133,7 @@ export default function CookieBannerTrackersPage({
|
|||||||
const [sourceFilter, setSourceFilter] = useState<CookieSource | null>(null);
|
const [sourceFilter, setSourceFilter] = useState<CookieSource | null>(null);
|
||||||
const [trackerTypeFilter, setTrackerTypeFilter] = useState<TrackerType | null>(null);
|
const [trackerTypeFilter, setTrackerTypeFilter] = useState<TrackerType | null>(null);
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||||
|
const [thirdPartyFilter, setThirdPartyFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: fragmentData, ...pagination } = usePaginationFragment<
|
const { data: fragmentData, ...pagination } = usePaginationFragment<
|
||||||
CookieBannerTrackersPageRefetchQuery,
|
CookieBannerTrackersPageRefetchQuery,
|
||||||
@@ -128,6 +142,11 @@ export default function CookieBannerTrackersPage({
|
|||||||
|
|
||||||
const connectionId = fragmentData.trackerPatterns.__id;
|
const connectionId = fragmentData.trackerPatterns.__id;
|
||||||
const patterns = fragmentData.trackerPatterns.edges.map(edge => edge.node) ?? [];
|
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"
|
const categories = data.node.__typename === "CookieBanner"
|
||||||
? data.node.categories.edges.map(edge => edge.node)
|
? data.node.categories.edges.map(edge => edge.node)
|
||||||
@@ -141,6 +160,7 @@ export default function CookieBannerTrackersPage({
|
|||||||
source: sourceFilter,
|
source: sourceFilter,
|
||||||
trackerType: trackerTypeFilter,
|
trackerType: trackerTypeFilter,
|
||||||
cookieCategoryId: categoryFilter,
|
cookieCategoryId: categoryFilter,
|
||||||
|
thirdPartyId: thirdPartyFilter,
|
||||||
...overrides,
|
...overrides,
|
||||||
},
|
},
|
||||||
{ fetchPolicy: "network-only" },
|
{ fetchPolicy: "network-only" },
|
||||||
@@ -170,6 +190,12 @@ export default function CookieBannerTrackersPage({
|
|||||||
refetchFilters({ cookieCategoryId: newCategory });
|
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 }) => {
|
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
|
||||||
pagination.refetch({
|
pagination.refetch({
|
||||||
order: { direction: order.direction, field: order.field as TrackerPatternOrderField },
|
order: { direction: order.direction, field: order.field as TrackerPatternOrderField },
|
||||||
@@ -177,6 +203,7 @@ export default function CookieBannerTrackersPage({
|
|||||||
source: sourceFilter,
|
source: sourceFilter,
|
||||||
trackerType: trackerTypeFilter,
|
trackerType: trackerTypeFilter,
|
||||||
cookieCategoryId: categoryFilter,
|
cookieCategoryId: categoryFilter,
|
||||||
|
thirdPartyId: thirdPartyFilter,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,6 +247,15 @@ export default function CookieBannerTrackersPage({
|
|||||||
<Option key={category.id} value={category.id}>{category.name}</Option>
|
<Option key={category.id} value={category.id}>{category.name}</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</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>
|
||||||
|
|
||||||
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
||||||
@@ -234,6 +270,7 @@ export default function CookieBannerTrackersPage({
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Type")}</Th>
|
<Th>{__("Type")}</Th>
|
||||||
<SortableTh field="NAME">{__("Name")}</SortableTh>
|
<SortableTh field="NAME">{__("Name")}</SortableTh>
|
||||||
|
<Th>{__("Third party")}</Th>
|
||||||
<SortableTh field="SOURCE">{__("Source")}</SortableTh>
|
<SortableTh field="SOURCE">{__("Source")}</SortableTh>
|
||||||
<Th>{__("Category")}</Th>
|
<Th>{__("Category")}</Th>
|
||||||
<SortableTh field="LAST_MATCHED_AT">{__("Last Matched")}</SortableTh>
|
<SortableTh field="LAST_MATCHED_AT">{__("Last Matched")}</SortableTh>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import { humanizeSeconds } from "@probo/helpers";
|
import { humanizeSeconds } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
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 { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
import type { TrackerPatternPropertiesSection_trackerPattern$key } from "#/__generated__/core/TrackerPatternPropertiesSection_trackerPattern.graphql";
|
import type { TrackerPatternPropertiesSection_trackerPattern$key } from "#/__generated__/core/TrackerPatternPropertiesSection_trackerPattern.graphql";
|
||||||
@@ -33,6 +33,15 @@ const trackerPatternPropertiesSectionFragment = graphql`
|
|||||||
cookieCategory {
|
cookieCategory {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
thirdParty {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
commonThirdParty {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -93,6 +102,26 @@ export function TrackerPatternPropertiesSection({
|
|||||||
{pattern.cookieCategory?.name ?? "-"}
|
{pattern.cookieCategory?.name ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
</PropertyRow>
|
</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")}>
|
<PropertyRow label={__("Max Age")}>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
|
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
|||||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
IconArrowBoxLeft,
|
IconArrowBoxLeft,
|
||||||
@@ -56,6 +57,15 @@ const trackerPatternFragment = graphql`
|
|||||||
cookieCategory {
|
cookieCategory {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
thirdParty {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
commonThirdParty {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -314,6 +324,26 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</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>
|
<Td>
|
||||||
{srcBadge
|
{srcBadge
|
||||||
? <Badge variant={srcBadge.variant}>{srcBadge.label}</Badge>
|
? <Badge variant={srcBadge.variant}>{srcBadge.label}</Badge>
|
||||||
|
|||||||
Reference in New Issue
Block a user