Add tracker type filter and color-coded badges

The trackers page now lets users filter by tracker type
(Cookie, localStorage, sessionStorage, IndexedDB, Cache
Storage) in addition to the existing source filter. Each
tracker type and cookie source badge uses a distinct color
for quick visual scanning.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-11 15:34:24 +04:00
parent 4aebf65b89
commit 71a33e412b
4 changed files with 70 additions and 21 deletions

View File

@@ -37,6 +37,7 @@ import type {
CookieBannerTrackersPageRefetchQuery,
CookieSource,
TrackerPatternOrderField,
TrackerType,
} from "#/__generated__/core/CookieBannerTrackersPageRefetchQuery.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable";
@@ -64,6 +65,7 @@ const trackersFragment = graphql`
last: { type: "Int", defaultValue: null }
query: { type: "String", defaultValue: null }
source: { type: "CookieSource", defaultValue: null }
trackerType: { type: "TrackerType", defaultValue: null }
) {
uncategorisedTrackerPatterns(
first: $first
@@ -71,7 +73,7 @@ const trackersFragment = graphql`
last: $last
before: $before
orderBy: $order
filter: { query: $query, source: $source }
filter: { query: $query, source: $source, trackerType: $trackerType }
)
@connection(
key: "CookieBannerTrackersPage_uncategorisedTrackerPatterns"
@@ -106,6 +108,7 @@ export default function CookieBannerTrackersPage({
const [isPending, startTransition] = useTransition();
const [queryFilter, setQueryFilter] = useState("");
const [sourceFilter, setSourceFilter] = useState<CookieSource | null>(null);
const [trackerTypeFilter, setTrackerTypeFilter] = useState<TrackerType | null>(null);
const { data: fragmentData, ...pagination } = usePaginationFragment<
CookieBannerTrackersPageRefetchQuery,
@@ -121,6 +124,7 @@ export default function CookieBannerTrackersPage({
{
query: queryFilter || null,
source: sourceFilter,
trackerType: trackerTypeFilter,
...overrides,
},
{ fetchPolicy: "network-only" },
@@ -138,11 +142,18 @@ export default function CookieBannerTrackersPage({
refetchFilters({ source: newSource });
};
const handleTrackerTypeFilterChange = (value: string) => {
const newType = value === "ALL" ? null : (value as TrackerType);
setTrackerTypeFilter(newType);
refetchFilters({ trackerType: newType });
};
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
pagination.refetch({
order: { direction: order.direction, field: order.field as TrackerPatternOrderField },
query: queryFilter || null,
source: sourceFilter,
trackerType: trackerTypeFilter,
});
};
@@ -165,6 +176,17 @@ export default function CookieBannerTrackersPage({
<Option value="SCRIPT">{__("Script")}</Option>
<Option value="PRE_EXISTING">{__("Pre-existing")}</Option>
</Select>
<Select
value={trackerTypeFilter ?? "ALL"}
onValueChange={handleTrackerTypeFilterChange}
>
<Option value="ALL">{__("All types")}</Option>
<Option value="COOKIE">{__("Cookie")}</Option>
<Option value="LOCAL_STORAGE">{__("localStorage")}</Option>
<Option value="SESSION_STORAGE">{__("sessionStorage")}</Option>
<Option value="INDEXED_DB">{__("IndexedDB")}</Option>
<Option value="CACHE_STORAGE">{__("Cache Storage")}</Option>
</Select>
</div>
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Eye as IconEye, EyeSlash as IconEyeSlash } from "@phosphor-icons/react";
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { formatDate, formatError, type GraphQLError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
@@ -124,14 +124,24 @@ const updatePatternMutation = graphql`
}
`;
function trackerTypeLabel(type: string, __: (s: string) => string): string {
type BadgeVariant = "success" | "warning" | "danger" | "info" | "neutral" | "outline" | "highlight";
function trackerTypeBadge(type: string, __: (s: string) => string): { label: string; variant: BadgeVariant } {
switch (type) {
case "COOKIE": return __("Cookie");
case "LOCAL_STORAGE": return __("localStorage");
case "SESSION_STORAGE": return __("sessionStorage");
case "INDEXED_DB": return __("IndexedDB");
case "CACHE_STORAGE": return __("Cache Storage");
default: return type;
case "COOKIE": return { label: __("Cookie"), variant: "warning" };
case "LOCAL_STORAGE": return { label: __("localStorage"), variant: "info" };
case "SESSION_STORAGE": return { label: __("sessionStorage"), variant: "highlight" };
case "INDEXED_DB": return { label: __("IndexedDB"), variant: "success" };
case "CACHE_STORAGE": return { label: __("Cache Storage"), variant: "outline" };
default: return { label: type, variant: "neutral" };
}
}
function sourceBadge(source: string, __: (s: string) => string): { label: string; variant: BadgeVariant } {
switch (source) {
case "SCRIPT": return { label: __("Script"), variant: "info" };
case "PRE_EXISTING": return { label: __("Pre-existing"), variant: "outline" };
default: return { label: source, variant: "neutral" };
}
}
@@ -286,6 +296,9 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
);
}
const typeBadge = trackerTypeBadge(pattern.trackerType, __);
const srcBadge = pattern.source ? sourceBadge(pattern.source, __) : null;
return (
<Tr className={pattern.excluded ? "bg-txt-quaternary opacity-80 line-through" : undefined}>
<Td>
@@ -299,17 +312,11 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
</div>
</Td>
<Td>
<Badge variant="neutral">
{trackerTypeLabel(pattern.trackerType, __)}
</Badge>
<Badge variant={typeBadge.variant}>{typeBadge.label}</Badge>
</Td>
<Td>
{pattern.source
? (
<Badge variant={pattern.source === "SCRIPT" ? "info" : "neutral"}>
{pattern.source === "SCRIPT" ? __("Script") : __("Pre-existing")}
</Badge>
)
{srcBadge
? <Badge variant={srcBadge.variant}>{srcBadge.label}</Badge>
: <span className="text-txt-tertiary">-</span>}
</Td>
<Td>
@@ -360,7 +367,7 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
className="p-1 rounded cursor-pointer"
title={pattern.excluded ? __("Include") : __("Exclude")}
>
{pattern.excluded ? <IconEye size={14} /> : <IconEyeSlash size={14} />}
{pattern.excluded ? <EyeIcon size={14} /> : <EyeSlashIcon size={14} />}
</button>
<button
type="button"

View File

@@ -25,6 +25,7 @@ type TrackerPatternFilter struct {
excluded *bool
query *string
source *CookieSource
trackerType *TrackerType
}
func NewTrackerPatternFilter(
@@ -49,6 +50,11 @@ func (f *TrackerPatternFilter) WithSource(source *CookieSource) *TrackerPatternF
return f
}
func (f *TrackerPatternFilter) WithTrackerType(trackerType *TrackerType) *TrackerPatternFilter {
f.trackerType = trackerType
return f
}
func (f *TrackerPatternFilter) SQLFragment() string {
if f == nil {
return "TRUE"
@@ -90,6 +96,13 @@ func (f *TrackerPatternFilter) SQLFragment() string {
source = @filter_source::cookie_source
ELSE TRUE
END
AND
CASE
WHEN @has_tracker_type_filter::boolean = false THEN TRUE
WHEN @has_tracker_type_filter::boolean = true THEN
tracker_type = @filter_tracker_type::tracker_type
ELSE TRUE
END
)`
}
@@ -108,6 +121,8 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
"filter_query": nil,
"has_source_filter": false,
"filter_source": nil,
"has_tracker_type_filter": false,
"filter_tracker_type": nil,
}
if f.matchType != nil {
@@ -134,5 +149,10 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
args["filter_source"] = string(*f.source)
}
if f.trackerType != nil {
args["has_tracker_type_filter"] = true
args["filter_tracker_type"] = string(*f.trackerType)
}
return args
}

View File

@@ -199,7 +199,7 @@ func (r *cookieBannerResolver) UncategorisedTrackerPatterns(ctx context.Context,
coredataFilter := coredata.NewTrackerPatternFilter(nil, nil, nil)
if filter != nil {
coredataFilter = coredataFilter.WithQuery(filter.Query).WithSource(filter.Source)
coredataFilter = coredataFilter.WithQuery(filter.Query).WithSource(filter.Source).WithTrackerType(filter.TrackerType)
}
patterns, err := r.cookieBanner.ListUncategorisedTrackerPatterns(ctx, scope, obj.ID, cursor, coredataFilter)
@@ -1220,7 +1220,7 @@ func (r *trackerPatternConnectionResolver) TotalCount(ctx context.Context, obj *
default:
filter := coredata.NewTrackerPatternFilter(nil, nil, nil)
if obj.Filter != nil {
filter = filter.WithQuery(obj.Filter.Query).WithSource(obj.Filter.Source)
filter = filter.WithQuery(obj.Filter.Query).WithSource(obj.Filter.Source).WithTrackerType(obj.Filter.TrackerType)
}
count, err = r.cookieBanner.CountUncategorisedTrackerPatterns(ctx, scope, obj.ParentID, filter)
}