Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions src/components/plugins/PluginResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Blocks } from "lucide-react";
import { useIntl } from "react-intl";

import { EmptyStatePlaceholder } from "@/components/dashboard/EmptyStatePlaceholder";
import { StatusDot } from "@/components/dashboard/StatusDot";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { CardTag } from "@/components/ui/card-tag";
Expand All @@ -18,8 +17,6 @@ import {
import type { PluginSummary } from "@/generated/types";
import { useDebouncedValue } from "@/hooks/useDebouncedValue";

const ENABLED_STATUS = "enabled";

enum PluginIconColor {
Red = "bg-red-500",
Orange = "bg-orange-500",
Expand Down Expand Up @@ -50,10 +47,6 @@ function hashString(str: string): number {
return Math.abs(hash);
}

function isEnabled(plugin: PluginSummary): boolean {
return plugin.status === ENABLED_STATUS;
}

const PluginIcon = memo(function PluginIcon({ name }: { name: string }) {
const colorClass = useMemo(() => {
const hash = hashString(name);
Expand Down Expand Up @@ -87,14 +80,6 @@ function PluginCard({
<CardContent className="flex flex-1 flex-col px-5 py-5">
<div className="flex items-start justify-between gap-3">
<PluginIcon name={plugin.name} />
<StatusDot
tone={isEnabled(plugin) ? "success" : "muted"}
className="text-sm text-muted-foreground"
>
{isEnabled(plugin)
? intl.formatMessage({ id: "plugins.catalog.enabled" })
: intl.formatMessage({ id: "plugins.catalog.disabled" })}
</StatusDot>
</div>

<h2 id={headingId} className="mt-4 truncate text-sm font-medium text-foreground">
Expand Down Expand Up @@ -183,11 +168,6 @@ export function PluginDetailsDialog({
<DetailRow label={intl.formatMessage({ id: "plugins.catalog.priority" })}>
{plugin.priority}
</DetailRow>
<DetailRow label={intl.formatMessage({ id: "plugins.catalog.status" })}>
{isEnabled(plugin)
? intl.formatMessage({ id: "plugins.catalog.enabled" })
: intl.formatMessage({ id: "plugins.catalog.disabled" })}
</DetailRow>
</dl>

{plugin.hooks && plugin.hooks.length > 0 && (
Expand Down
33 changes: 1 addition & 32 deletions src/components/plugins/PluginToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ import {

const ALL_FILTER_VALUE = "__all__";

export type PluginSingleFilterKey = "mode" | "hook";
export type PluginSingleFilterKey = "hook";

interface PluginToolbarProps {
search: string;
enabledOnly: boolean;
mode: string;
hook: string;
selectedTags: string[];
modes: string[];
hooks: string[];
availableTags: string[];
activeFilterCount: number;
Expand Down Expand Up @@ -75,10 +73,8 @@ function PluginViewToggle({
}

function PluginFiltersPopover({
mode,
hook,
selectedTags,
modes,
hooks,
availableTags,
activeFilterCount,
Expand All @@ -89,7 +85,6 @@ function PluginFiltersPopover({
const intl = useIntl();
const id = useId();
const filtersTitleId = `${id}-title`;
const modeTriggerId = `${id}-mode`;
const hookTriggerId = `${id}-hook`;

return (
Expand Down Expand Up @@ -126,32 +121,6 @@ function PluginFiltersPopover({
)}
</div>

<div className="space-y-2">
<Label htmlFor={modeTriggerId} className="text-xs">
{intl.formatMessage({ id: "plugins.catalog.mode" })}
</Label>
<Select
value={mode || ALL_FILTER_VALUE}
onValueChange={(value) =>
onSetSingleFilter("mode", value === ALL_FILTER_VALUE ? null : value)
}
>
<SelectTrigger id={modeTriggerId} className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_FILTER_VALUE}>
{intl.formatMessage({ id: "plugins.catalog.allModes" })}
</SelectItem>
{modes.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor={hookTriggerId} className="text-xs">
{intl.formatMessage({ id: "plugins.catalog.hook" })}
Expand Down
3 changes: 0 additions & 3 deletions src/i18n/locales/en-US/plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@
"plugins.catalog.mode": "Mode",
"plugins.catalog.hook": "Hook",
"plugins.catalog.tags": "Tags",
"plugins.catalog.allModes": "All modes",
"plugins.catalog.allHooks": "All hooks",
"plugins.catalog.allTags": "All tags",
"plugins.catalog.enabled": "Enabled",
"plugins.catalog.disabled": "Disabled",
"plugins.catalog.view": "View",
"plugins.catalog.viewPlugin": "View {name}",
"plugins.catalog.viewOptions": "Plugins view",
"plugins.catalog.author": "Author",
"plugins.catalog.version": "Version",
"plugins.catalog.priority": "Priority",
"plugins.catalog.status": "Status",
"plugins.catalog.hooks": "Hooks",
"plugins.catalog.configuration": "Configuration",
"plugins.catalog.resultCount": "{count, plural, =0 {No plugins shown} one {# plugin shown} other {# plugins shown}}",
Expand Down
3 changes: 0 additions & 3 deletions src/i18n/locales/es-ES/plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@
"plugins.catalog.mode": "Modo",
"plugins.catalog.hook": "Hook",
"plugins.catalog.tags": "Etiquetas",
"plugins.catalog.allModes": "Todos los modos",
"plugins.catalog.allHooks": "Todos los hooks",
"plugins.catalog.allTags": "Todas las etiquetas",
"plugins.catalog.enabled": "Habilitado",
"plugins.catalog.disabled": "Deshabilitado",
"plugins.catalog.view": "Ver",
"plugins.catalog.viewPlugin": "Ver {name}",
"plugins.catalog.viewOptions": "Vista de plugins",
"plugins.catalog.author": "Autor",
"plugins.catalog.version": "VersiΓ³n",
"plugins.catalog.priority": "Prioridad",
"plugins.catalog.status": "Estado",
"plugins.catalog.hooks": "Hooks",
"plugins.catalog.configuration": "ConfiguraciΓ³n",
"plugins.catalog.resultCount": "{count, plural, =0 {No hay plugins mostrados} one {# plugin mostrado} other {# plugins mostrados}}",
Expand Down
3 changes: 0 additions & 3 deletions src/i18n/locales/pt-BR/plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@
"plugins.catalog.mode": "Modo",
"plugins.catalog.hook": "Hook",
"plugins.catalog.tags": "Tags",
"plugins.catalog.allModes": "Todos os modos",
"plugins.catalog.allHooks": "Todos os hooks",
"plugins.catalog.allTags": "Todas as tags",
"plugins.catalog.enabled": "Habilitado",
"plugins.catalog.disabled": "Desabilitado",
"plugins.catalog.view": "Ver",
"plugins.catalog.viewPlugin": "Ver {name}",
"plugins.catalog.viewOptions": "VisualizaΓ§Γ£o de plugins",
"plugins.catalog.author": "Autor",
"plugins.catalog.version": "VersΓ£o",
"plugins.catalog.priority": "Prioridade",
"plugins.catalog.status": "Status",
"plugins.catalog.hooks": "Hooks",
"plugins.catalog.configuration": "ConfiguraΓ§Γ£o",
"plugins.catalog.resultCount": "{count, plural, =0 {Nenhum plugin exibido} one {# plugin exibido} other {# plugins exibidos}}",
Expand Down
11 changes: 11 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,14 @@
overflow-y: scroll;
}
}

/* Radix dialogs lock the page through react-remove-scroll, which puts a
margin-right the width of the scrollbar on the body to compensate for the
scrollbar it expects to hide. Both rules above keep that space reserved for
the whole session, so the compensation double-counts and shifts the page left
as a dialog opens. Zero it out; the gutter already holds the space.
The injected rule is `body[data-scroll-locked] { margin-right: Npx !important }`
and lands later in the cascade, so this needs the extra specificity to win. */
html body[data-scroll-locked] {
margin-right: 0 !important;
}
25 changes: 15 additions & 10 deletions src/pages/Plugins.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const logging: PluginSummary = {
description: "Logs request and response payloads for auditing",
author: "Community",
version: "0.4.1",
mode: "permissive",
mode: "disabled",
priority: 50,
hooks: ["http_pre_request"],
tags: ["observability"],
Expand Down Expand Up @@ -84,16 +84,21 @@ describe("Plugins", () => {
expect(screen.getByRole("status", { name: "Loading..." })).toBeInTheDocument();
});

it("renders plugin cards with status", () => {
it("renders plugin cards without a status badge", () => {
renderWithRouter(<Plugins />);

expect(screen.getByRole("region", { name: "Plugins" })).toBeInTheDocument();
const list = screen.getByRole("list", { name: "Catalog plugins" });
expect(within(list).getAllByRole("listitem")).toHaveLength(2);
expect(screen.getByRole("heading", { name: "PII Guardrails" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Request Logger" })).toBeInTheDocument();
expect(within(list).getByText("Enabled")).toBeInTheDocument();
expect(within(list).getByText("Disabled")).toBeInTheDocument();
expect(
within(list).getByText("Detects and redacts personally identifiable information"),
).toBeInTheDocument();
// Cards carry no status until enable/disable lands, at which point only
// enabled plugins get a badge.
expect(within(list).queryByText("Enabled")).not.toBeInTheDocument();
expect(within(list).queryByText("Disabled")).not.toBeInTheDocument();
expect(screen.getByRole("status")).toHaveTextContent("2 plugins shown");
});

Expand All @@ -112,6 +117,9 @@ describe("Plugins", () => {
expect(within(dialog).getByText("redact")).toBeInTheDocument();
expect(within(dialog).getByText("true")).toBeInTheDocument();
expect(within(dialog).getByText('["EMAIL","SSN"]')).toBeInTheDocument();
// Status is derived from mode (status === "disabled" iff mode === "disabled"),
// so the dialog shows mode only.
expect(within(dialog).queryByText("Status")).not.toBeInTheDocument();

await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
Expand All @@ -128,17 +136,14 @@ describe("Plugins", () => {
expect(screen.getByText("No plugins match the active search and filters.")).toBeInTheDocument();
});

it("filters by mode and reflects it in the URL", async () => {
it("offers no mode filter β€” hook and tags only", async () => {
const user = userEvent.setup();
renderWithRouter(<Plugins />);

await user.click(screen.getByRole("button", { name: /^Filters$/ }));
await user.click(screen.getByRole("combobox", { name: "Mode" }));
await user.click(screen.getByRole("option", { name: "enforce" }));

await waitFor(() => expect(window.location.search).toContain("mode=enforce"));
expect(screen.getByRole("heading", { name: "PII Guardrails" })).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Request Logger" })).not.toBeInTheDocument();
expect(screen.getByRole("combobox", { name: "Hook" })).toBeInTheDocument();
expect(screen.queryByRole("combobox", { name: "Mode" })).not.toBeInTheDocument();
});

it("filters by hook and tag, then clears filters", async () => {
Expand Down
17 changes: 2 additions & 15 deletions src/pages/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const PAGE_HEADING_ID = "plugins-catalog-heading";

interface PluginFilters {
search: string;
mode: string;
hook: string;
tags: string[];
enabledOnly: boolean;
Expand All @@ -38,7 +37,6 @@ function parseFilters(path: string): PluginFilters {

return {
search: params.get("search") ?? "",
mode: params.get("mode") ?? "",
hook: params.get("hook") ?? "",
tags: [...new Set(params.getAll("tags").filter(Boolean))],
enabledOnly: params.get("status") === ENABLED_STATUS,
Expand Down Expand Up @@ -83,10 +81,7 @@ function usePluginFilters() {
[filters.tags, updateQuery],
);

const clearFilters = useCallback(
() => updateQuery({ mode: null, hook: null, tags: [] }),
[updateQuery],
);
const clearFilters = useCallback(() => updateQuery({ hook: null, tags: [] }), [updateQuery]);

return { filters, updateQuery, setSingleFilter, toggleTag, clearFilters };
}
Expand All @@ -95,7 +90,6 @@ function filterPlugins(plugins: PluginSummary[], filters: PluginFilters): Plugin
const search = filters.search.trim().toLocaleLowerCase();

return plugins.filter((plugin) => {
if (filters.mode && plugin.mode !== filters.mode) return false;
if (filters.hook && !plugin.hooks?.includes(filters.hook)) return false;
if (filters.tags.length > 0 && !filters.tags.some((tag) => plugin.tags?.includes(tag))) {
return false;
Expand Down Expand Up @@ -150,10 +144,6 @@ export function Plugins() {
() => filterPlugins(allPlugins, activeFilters),
[allPlugins, activeFilters],
);
const modeOptions = useMemo(
() => sortedUnique(allPlugins.map((plugin) => plugin.mode)),
[allPlugins],
);
const hookOptions = useMemo(
() => sortedUnique(allPlugins.flatMap((plugin) => plugin.hooks ?? [])),
[allPlugins],
Expand All @@ -169,8 +159,7 @@ export function Plugins() {
: filters.enabledOnly && !hasEnabledPlugins
? "plugins.catalog.noneEnabled"
: "plugins.catalog.noResults";
const activeFilterCount =
Number(Boolean(filters.mode)) + Number(Boolean(filters.hook)) + filters.tags.length;
const activeFilterCount = Number(Boolean(filters.hook)) + filters.tags.length;

const handleView = useCallback((plugin: PluginSummary, trigger: HTMLButtonElement) => {
lastViewTriggerRef.current = trigger;
Expand Down Expand Up @@ -229,10 +218,8 @@ export function Plugins() {
<PluginToolbar
search={search}
enabledOnly={filters.enabledOnly}
mode={filters.mode}
hook={filters.hook}
selectedTags={filters.tags}
modes={modeOptions}
hooks={hookOptions}
availableTags={tagOptions}
activeFilterCount={activeFilterCount}
Expand Down
Loading