import { Plus, X } from "lucide-react"; import { useMemo, useState } from "react"; import Button from "~/components/button"; import Input from "~/components/input"; import TableList from "~/components/table-list"; import cn from "~/utils/cn"; interface TokenListProps { label: string; description?: string; values: string[]; onChange: (values: string[]) => void; suggestions?: string[]; placeholder?: string; emptyText: string; isDisabled?: boolean; validate?: (value: string) => boolean; // Rewrites a value before it is added, e.g. appending a default port. normalize?: (value: string) => string; } // A small chip editor shared by the ACL and user dialogs, modelled on the // machine tag dialog: current values, a field to add one, and suggestions. export default function TokenList({ label, description, values, onChange, suggestions, placeholder, emptyText, isDisabled, validate, normalize, }: TokenListProps) { const [draft, setDraft] = useState(""); const prepare = useMemo( () => (value: string) => (normalize ? normalize(value.trim()) : value.trim()), [normalize], ); // Suggestions are compared in their normalized form, otherwise picking // `tag:web` after it was added as `tag:web:*` would duplicate it. const available = useMemo( () => (suggestions ?? []).filter((suggestion) => !values.includes(prepare(suggestion))), [suggestions, values, prepare], ); const draftIsInvalid = useMemo(() => { const prepared = prepare(draft); if (prepared.length === 0) return true; if (values.includes(prepared)) return true; return validate ? !validate(prepared) : false; }, [draft, values, validate, prepare]); function add(value: string) { const prepared = prepare(value); if (prepared.length === 0 || values.includes(prepared)) { return; } onChange([...values, prepared]); setDraft(""); } return (
{label}
{description ? ({description}
) : null}