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:
@@ -37,6 +37,7 @@ import type {
|
|||||||
CookieBannerTrackersPageRefetchQuery,
|
CookieBannerTrackersPageRefetchQuery,
|
||||||
CookieSource,
|
CookieSource,
|
||||||
TrackerPatternOrderField,
|
TrackerPatternOrderField,
|
||||||
|
TrackerType,
|
||||||
} from "#/__generated__/core/CookieBannerTrackersPageRefetchQuery.graphql";
|
} from "#/__generated__/core/CookieBannerTrackersPageRefetchQuery.graphql";
|
||||||
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ const trackersFragment = graphql`
|
|||||||
last: { type: "Int", defaultValue: null }
|
last: { type: "Int", defaultValue: null }
|
||||||
query: { type: "String", defaultValue: null }
|
query: { type: "String", defaultValue: null }
|
||||||
source: { type: "CookieSource", defaultValue: null }
|
source: { type: "CookieSource", defaultValue: null }
|
||||||
|
trackerType: { type: "TrackerType", defaultValue: null }
|
||||||
) {
|
) {
|
||||||
uncategorisedTrackerPatterns(
|
uncategorisedTrackerPatterns(
|
||||||
first: $first
|
first: $first
|
||||||
@@ -71,7 +73,7 @@ const trackersFragment = graphql`
|
|||||||
last: $last
|
last: $last
|
||||||
before: $before
|
before: $before
|
||||||
orderBy: $order
|
orderBy: $order
|
||||||
filter: { query: $query, source: $source }
|
filter: { query: $query, source: $source, trackerType: $trackerType }
|
||||||
)
|
)
|
||||||
@connection(
|
@connection(
|
||||||
key: "CookieBannerTrackersPage_uncategorisedTrackerPatterns"
|
key: "CookieBannerTrackersPage_uncategorisedTrackerPatterns"
|
||||||
@@ -106,6 +108,7 @@ export default function CookieBannerTrackersPage({
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [queryFilter, setQueryFilter] = useState("");
|
const [queryFilter, setQueryFilter] = useState("");
|
||||||
const [sourceFilter, setSourceFilter] = useState<CookieSource | null>(null);
|
const [sourceFilter, setSourceFilter] = useState<CookieSource | null>(null);
|
||||||
|
const [trackerTypeFilter, setTrackerTypeFilter] = useState<TrackerType | null>(null);
|
||||||
|
|
||||||
const { data: fragmentData, ...pagination } = usePaginationFragment<
|
const { data: fragmentData, ...pagination } = usePaginationFragment<
|
||||||
CookieBannerTrackersPageRefetchQuery,
|
CookieBannerTrackersPageRefetchQuery,
|
||||||
@@ -121,6 +124,7 @@ export default function CookieBannerTrackersPage({
|
|||||||
{
|
{
|
||||||
query: queryFilter || null,
|
query: queryFilter || null,
|
||||||
source: sourceFilter,
|
source: sourceFilter,
|
||||||
|
trackerType: trackerTypeFilter,
|
||||||
...overrides,
|
...overrides,
|
||||||
},
|
},
|
||||||
{ fetchPolicy: "network-only" },
|
{ fetchPolicy: "network-only" },
|
||||||
@@ -138,11 +142,18 @@ export default function CookieBannerTrackersPage({
|
|||||||
refetchFilters({ source: newSource });
|
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 }) => {
|
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 },
|
||||||
query: queryFilter || null,
|
query: queryFilter || null,
|
||||||
source: sourceFilter,
|
source: sourceFilter,
|
||||||
|
trackerType: trackerTypeFilter,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,6 +176,17 @@ export default function CookieBannerTrackersPage({
|
|||||||
<Option value="SCRIPT">{__("Script")}</Option>
|
<Option value="SCRIPT">{__("Script")}</Option>
|
||||||
<Option value="PRE_EXISTING">{__("Pre-existing")}</Option>
|
<Option value="PRE_EXISTING">{__("Pre-existing")}</Option>
|
||||||
</Select>
|
</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>
|
||||||
|
|
||||||
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// 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 { formatDate, formatError, type GraphQLError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
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) {
|
switch (type) {
|
||||||
case "COOKIE": return __("Cookie");
|
case "COOKIE": return { label: __("Cookie"), variant: "warning" };
|
||||||
case "LOCAL_STORAGE": return __("localStorage");
|
case "LOCAL_STORAGE": return { label: __("localStorage"), variant: "info" };
|
||||||
case "SESSION_STORAGE": return __("sessionStorage");
|
case "SESSION_STORAGE": return { label: __("sessionStorage"), variant: "highlight" };
|
||||||
case "INDEXED_DB": return __("IndexedDB");
|
case "INDEXED_DB": return { label: __("IndexedDB"), variant: "success" };
|
||||||
case "CACHE_STORAGE": return __("Cache Storage");
|
case "CACHE_STORAGE": return { label: __("Cache Storage"), variant: "outline" };
|
||||||
default: return type;
|
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 (
|
return (
|
||||||
<Tr className={pattern.excluded ? "bg-txt-quaternary opacity-80 line-through" : undefined}>
|
<Tr className={pattern.excluded ? "bg-txt-quaternary opacity-80 line-through" : undefined}>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -299,17 +312,11 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
|||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge variant="neutral">
|
<Badge variant={typeBadge.variant}>{typeBadge.label}</Badge>
|
||||||
{trackerTypeLabel(pattern.trackerType, __)}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{pattern.source
|
{srcBadge
|
||||||
? (
|
? <Badge variant={srcBadge.variant}>{srcBadge.label}</Badge>
|
||||||
<Badge variant={pattern.source === "SCRIPT" ? "info" : "neutral"}>
|
|
||||||
{pattern.source === "SCRIPT" ? __("Script") : __("Pre-existing")}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
: <span className="text-txt-tertiary">-</span>}
|
: <span className="text-txt-tertiary">-</span>}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -360,7 +367,7 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
|||||||
className="p-1 rounded cursor-pointer"
|
className="p-1 rounded cursor-pointer"
|
||||||
title={pattern.excluded ? __("Include") : __("Exclude")}
|
title={pattern.excluded ? __("Include") : __("Exclude")}
|
||||||
>
|
>
|
||||||
{pattern.excluded ? <IconEye size={14} /> : <IconEyeSlash size={14} />}
|
{pattern.excluded ? <EyeIcon size={14} /> : <EyeSlashIcon size={14} />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type TrackerPatternFilter struct {
|
|||||||
excluded *bool
|
excluded *bool
|
||||||
query *string
|
query *string
|
||||||
source *CookieSource
|
source *CookieSource
|
||||||
|
trackerType *TrackerType
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTrackerPatternFilter(
|
func NewTrackerPatternFilter(
|
||||||
@@ -49,6 +50,11 @@ func (f *TrackerPatternFilter) WithSource(source *CookieSource) *TrackerPatternF
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TrackerPatternFilter) WithTrackerType(trackerType *TrackerType) *TrackerPatternFilter {
|
||||||
|
f.trackerType = trackerType
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
func (f *TrackerPatternFilter) SQLFragment() string {
|
func (f *TrackerPatternFilter) SQLFragment() string {
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return "TRUE"
|
return "TRUE"
|
||||||
@@ -90,6 +96,13 @@ func (f *TrackerPatternFilter) SQLFragment() string {
|
|||||||
source = @filter_source::cookie_source
|
source = @filter_source::cookie_source
|
||||||
ELSE TRUE
|
ELSE TRUE
|
||||||
END
|
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,
|
"filter_query": nil,
|
||||||
"has_source_filter": false,
|
"has_source_filter": false,
|
||||||
"filter_source": nil,
|
"filter_source": nil,
|
||||||
|
"has_tracker_type_filter": false,
|
||||||
|
"filter_tracker_type": nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.matchType != nil {
|
if f.matchType != nil {
|
||||||
@@ -134,5 +149,10 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
|
|||||||
args["filter_source"] = string(*f.source)
|
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
|
return args
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ func (r *cookieBannerResolver) UncategorisedTrackerPatterns(ctx context.Context,
|
|||||||
|
|
||||||
coredataFilter := coredata.NewTrackerPatternFilter(nil, nil, nil)
|
coredataFilter := coredata.NewTrackerPatternFilter(nil, nil, nil)
|
||||||
if filter != 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)
|
patterns, err := r.cookieBanner.ListUncategorisedTrackerPatterns(ctx, scope, obj.ID, cursor, coredataFilter)
|
||||||
@@ -1220,7 +1220,7 @@ func (r *trackerPatternConnectionResolver) TotalCount(ctx context.Context, obj *
|
|||||||
default:
|
default:
|
||||||
filter := coredata.NewTrackerPatternFilter(nil, nil, nil)
|
filter := coredata.NewTrackerPatternFilter(nil, nil, nil)
|
||||||
if obj.Filter != 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)
|
count, err = r.cookieBanner.CountUncategorisedTrackerPatterns(ctx, scope, obj.ParentID, filter)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user