Display cookie pattern source badge in category table

Show the origin of each cookie pattern (Script vs Pre-existing)
as a badge with a tooltip in the cookies configuration table.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-01 17:41:50 +04:00
parent bb7bc7c716
commit 1d08760d3b
4 changed files with 109 additions and 16 deletions

View File

@@ -51,6 +51,7 @@ export function AddCookieRow({
name: data.name, name: data.name,
maxAgeSeconds: toMaxAgeSeconds(data.duration.value, data.duration.unit), maxAgeSeconds: toMaxAgeSeconds(data.duration.value, data.duration.unit),
description: data.description, description: data.description,
excluded: false,
}); });
}; };
@@ -62,6 +63,7 @@ export function AddCookieRow({
placeholder={__("Cookie name")} placeholder={__("Cookie name")}
/> />
</Td> </Td>
<Td />
<Td className="pr-3"> <Td className="pr-3">
<Controller <Controller
name="duration" name="duration"

View File

@@ -28,6 +28,7 @@ import {
Td, Td,
Th, Th,
Thead, Thead,
Toggle,
Tr, Tr,
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
@@ -50,6 +51,7 @@ export interface CookieEntry {
name: string; name: string;
maxAgeSeconds: number | null; maxAgeSeconds: number | null;
description: string; description: string;
excluded: boolean;
} }
export const categorySectionFragment = graphql` export const categorySectionFragment = graphql`
@@ -71,6 +73,8 @@ export const categorySectionFragment = graphql`
displayName displayName
maxAgeSeconds maxAgeSeconds
description description
excluded
source
...EditCookieRowFragment ...EditCookieRowFragment
} }
} }
@@ -127,6 +131,8 @@ const createPatternMutation = graphql`
displayName displayName
maxAgeSeconds maxAgeSeconds
description description
excluded
source
...EditCookieRowFragment ...EditCookieRowFragment
} }
} }
@@ -152,6 +158,7 @@ const updatePatternMutation = graphql`
displayName displayName
maxAgeSeconds maxAgeSeconds
description description
excluded
updatedAt updatedAt
} }
cookieBanner { cookieBanner {
@@ -342,6 +349,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
displayName: cookie.name, displayName: cookie.name,
maxAgeSeconds: cookie.maxAgeSeconds, maxAgeSeconds: cookie.maxAgeSeconds,
description: cookie.description, description: cookie.description,
excluded: cookie.excluded,
}, },
}, },
onCompleted(_response, errors) { onCompleted(_response, errors) {
@@ -378,6 +386,37 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
}); });
}; };
const handleToggleExcluded = (patternId: string, excluded: boolean) => {
updatePattern({
variables: {
input: {
cookiePatternId: patternId,
excluded,
},
},
onCompleted(_response, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: errors[0].message,
variant: "error",
});
return;
}
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to update cookie"),
error as GraphQLError,
),
variant: "error",
});
},
});
};
const handleDeleteCookie = (patternId: string) => { const handleDeleteCookie = (patternId: string) => {
deletePattern({ deletePattern({
variables: { variables: {
@@ -568,7 +607,8 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
<table className="w-full text-left"> <table className="w-full text-left">
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th><span className="pl-10">{__("Name")}</span></Th>
<Th>{__("Source")}</Th>
<Th>{__("Duration")}</Th> <Th>{__("Duration")}</Th>
<Th>{__("Description")}</Th> <Th>{__("Description")}</Th>
<Th className="w-20" /> <Th className="w-20" />
@@ -587,9 +627,30 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
/> />
) )
: ( : (
<Tr key={pattern.id}> <Tr key={pattern.id} className={pattern.excluded ? "opacity-50" : undefined}>
<Td> <Td>
<code className="text-sm font-mono">{pattern.displayName}</code> <div className="flex items-center gap-2">
<Toggle
size="sm"
checked={!pattern.excluded}
onChange={() => handleToggleExcluded(pattern.id, !pattern.excluded)}
disabled={isUpdatingPattern}
title={__("Include this cookie in the banner")}
/>
<code className="text-sm font-mono">{pattern.displayName}</code>
</div>
</Td>
<Td>
<Badge
variant={pattern.source === "SCRIPT" ? "info" : "neutral"}
title={
pattern.source === "SCRIPT"
? __("Set by a script at runtime")
: __("Already present when the page was loaded")
}
>
{pattern.source === "SCRIPT" ? __("Script") : __("Pre-existing")}
</Badge>
</Td> </Td>
<Td className="text-sm text-muted-foreground"> <Td className="text-sm text-muted-foreground">
{humanizeSeconds(pattern.maxAgeSeconds ?? null)} {humanizeSeconds(pattern.maxAgeSeconds ?? null)}

View File

@@ -14,7 +14,7 @@
import { fromMaxAgeSeconds, toMaxAgeSeconds } from "@probo/helpers"; import { fromMaxAgeSeconds, toMaxAgeSeconds } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Button, DurationInput, Input, Td, Tr } from "@probo/ui"; import { Button, DurationInput, Input, Td, Toggle, Tr } from "@probo/ui";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -28,6 +28,7 @@ export const editCookieRowFragment = graphql`
displayName displayName
maxAgeSeconds maxAgeSeconds
description description
excluded
} }
`; `;
@@ -35,6 +36,7 @@ interface CookieFormValues {
name: string; name: string;
duration: { value: string; unit: string }; duration: { value: string; unit: string };
description: string; description: string;
excluded: boolean;
} }
interface EditCookieRowProps { interface EditCookieRowProps {
@@ -59,6 +61,7 @@ export function EditCookieRow({
name: cookie.displayName, name: cookie.displayName,
duration: initial, duration: initial,
description: cookie.description, description: cookie.description,
excluded: cookie.excluded,
}, },
}); });
@@ -67,17 +70,33 @@ export function EditCookieRow({
name: data.name, name: data.name,
maxAgeSeconds: toMaxAgeSeconds(data.duration.value, data.duration.unit), maxAgeSeconds: toMaxAgeSeconds(data.duration.value, data.duration.unit),
description: data.description, description: data.description,
excluded: data.excluded,
}); });
}; };
return ( return (
<Tr> <Tr>
<Td className="pr-3"> <Td className="pr-3">
<Input <div className="flex items-center gap-2">
{...register("name")} <Controller
placeholder={__("Cookie name")} name="excluded"
/> control={control}
render={({ field }) => (
<Toggle
size="sm"
checked={!field.value}
onChange={checked => field.onChange(!checked)}
title={__("Include this cookie in the banner")}
/>
)}
/>
<Input
{...register("name")}
placeholder={__("Cookie name")}
/>
</div>
</Td> </Td>
<Td />
<Td className="pr-3"> <Td className="pr-3">
<Controller <Controller
name="duration" name="duration"
@@ -99,7 +118,7 @@ export function EditCookieRow({
/> />
</Td> </Td>
<Td> <Td>
<div className="flex items-center gap-1"> <div className="flex items-center gap-2">
<Button <Button
onClick={() => void handleSubmit(onSubmit)()} onClick={() => void handleSubmit(onSubmit)()}
disabled={isUpdating} disabled={isUpdating}

View File

@@ -12,28 +12,39 @@
// 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.
const sizes = {
default: { track: { width: 44, height: 24, padding: 2 }, thumb: 20 },
sm: { track: { width: 32, height: 18, padding: 2 }, thumb: 14 },
} as const;
type Props = { type Props = {
checked: boolean; checked: boolean;
onChange: (checked: boolean) => void; onChange: (checked: boolean) => void;
disabled?: boolean; disabled?: boolean;
size?: keyof typeof sizes;
title?: string;
}; };
export function Toggle({ checked, onChange, disabled = false }: Props) { export function Toggle({ checked, onChange, disabled = false, size = "default", title }: Props) {
const { track, thumb } = sizes[size];
const travel = track.width - thumb - track.padding * 2;
return ( return (
<button <button
type="button" type="button"
role="switch" role="switch"
aria-checked={checked} aria-checked={checked}
disabled={disabled} disabled={disabled}
title={title}
onClick={() => !disabled && onChange(!checked)} onClick={() => !disabled && onChange(!checked)}
style={{ style={{
position: "relative", position: "relative",
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
flexShrink: 0, flexShrink: 0,
width: 44, width: track.width,
height: 24, height: track.height,
padding: 2, padding: track.padding,
borderRadius: 9999, borderRadius: 9999,
border: "none", border: "none",
cursor: disabled ? "not-allowed" : "pointer", cursor: disabled ? "not-allowed" : "pointer",
@@ -47,13 +58,13 @@ export function Toggle({ checked, onChange, disabled = false }: Props) {
<span <span
style={{ style={{
display: "block", display: "block",
width: 20, width: thumb,
height: 20, height: thumb,
borderRadius: 9999, borderRadius: 9999,
backgroundColor: "white", backgroundColor: "white",
boxShadow: "0 1px 2px rgba(0,0,0,0.1)", boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
transition: "transform 200ms ease-in-out", transition: "transform 200ms ease-in-out",
transform: checked ? "translateX(20px)" : "translateX(0)", transform: checked ? `translateX(${travel}px)` : "translateX(0)",
}} }}
/> />
</button> </button>