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,
|
||||
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" : ""}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user