File size: 3,276 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | import { Search, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import type { PluginStatusFilter } from "./build-plugins-view-model";
interface PluginsToolbarProps {
search: string;
onSearchChange: (value: string) => void;
statusFilter: PluginStatusFilter;
onStatusFilterChange: (filter: PluginStatusFilter) => void;
}
const STATUS_FILTERS: ReadonlyArray<{
value: PluginStatusFilter;
labelKey: I18nKey;
}> = [
{ value: "all", labelKey: I18nKey.SETTINGS$PLUGINS_FILTER_ALL },
{ value: "installed", labelKey: I18nKey.SETTINGS$PLUGINS_FILTER_INSTALLED },
{ value: "available", labelKey: I18nKey.SETTINGS$PLUGINS_FILTER_AVAILABLE },
{ value: "local", labelKey: I18nKey.SETTINGS$PLUGINS_FILTER_LOCAL },
];
export function PluginsToolbar({
search,
onSearchChange,
statusFilter,
onStatusFilterChange,
}: PluginsToolbarProps) {
const { t } = useTranslation("openhands");
return (
<div data-testid="plugins-toolbar" className="flex items-stretch gap-2">
<div
className={cn(
"relative flex flex-1 min-w-0 items-center",
"rounded-lg border border-[var(--oh-border)] bg-base-secondary",
"focus-within:border-white/40 focus-within:ring-1 focus-within:ring-white/20",
"transition-colors",
)}
>
<Search
className="ml-3 h-4 w-4 shrink-0 text-tertiary-alt"
aria-hidden
/>
<input
data-testid="plugins-search-input"
type="search"
value={search}
onChange={(e) => onSearchChange(e.target.value)}
placeholder={t(I18nKey.SETTINGS$PLUGINS_SEARCH_PLACEHOLDER)}
aria-label={t(I18nKey.SETTINGS$PLUGINS_SEARCH_PLACEHOLDER)}
className={cn(
"flex-1 min-w-0 bg-transparent border-0 outline-none",
"px-3 py-2 text-sm placeholder:text-tertiary-alt",
"[&::-webkit-search-cancel-button]:hidden",
)}
/>
{search ? (
<button
type="button"
onClick={() => onSearchChange("")}
aria-label={t(I18nKey.MCP$SEARCH_CLEAR)}
className="mr-2 p-1 rounded text-tertiary-alt hover:text-white cursor-pointer"
>
<X className="h-4 w-4" aria-hidden />
</button>
) : null}
</div>
<div
data-testid="plugins-status-filter"
className="flex shrink-0 items-center gap-1 rounded-lg border border-[var(--oh-border)] bg-base-secondary p-1"
>
{STATUS_FILTERS.map((filter) => (
<button
key={filter.value}
type="button"
data-testid={`plugins-filter-${filter.value}`}
aria-pressed={statusFilter === filter.value}
onClick={() => onStatusFilterChange(filter.value)}
className={cn(
"cursor-pointer rounded-md px-3 py-1 text-xs transition-colors",
statusFilter === filter.value
? "bg-surface-raised text-white"
: "text-tertiary-alt hover:text-white",
)}
>
{t(filter.labelKey)}
</button>
))}
</div>
</div>
);
}
|