import { useEffect, useState, useCallback } from 'react'; import { Plus, Tag } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui/toast-store'; import { addNodeLabel, removeNodeLabel, getLabelsForNode, listDistinctLabels, } from '@/lib/blueprintsApi'; import { NodeLabelPill } from './NodeLabelPill'; interface NodeLabelPickerProps { nodeId: number; /** Whether the current operator is allowed to add/remove labels (admin + paid). */ canEdit?: boolean; /** Called whenever the label set for this node changes; parents may use this * to refresh their cached list. */ onChange?: (labels: string[]) => void; } export function NodeLabelPicker({ nodeId, canEdit = true, onChange }: NodeLabelPickerProps) { const [labels, setLabels] = useState([]); const [allLabels, setAllLabels] = useState([]); const [open, setOpen] = useState(false); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(false); const refresh = useCallback(async () => { setLoading(true); try { const [mine, distinct] = await Promise.all([ getLabelsForNode(nodeId), listDistinctLabels().catch(() => [] as string[]), ]); setLabels(mine); setAllLabels(distinct); onChange?.(mine); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to load node labels'); } finally { setLoading(false); } }, [nodeId, onChange]); useEffect(() => { void refresh(); }, [refresh]); async function handleAdd(label: string) { const trimmed = label.trim(); if (!trimmed) return; setBusy(true); try { await addNodeLabel(nodeId, trimmed); setInput(''); await refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add label'); } finally { setBusy(false); } } async function handleRemove(label: string) { setBusy(true); try { await removeNodeLabel(nodeId, label); await refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to remove label'); } finally { setBusy(false); } } const suggestions = allLabels.filter(l => !labels.includes(l) && (input === '' || l.toLowerCase().includes(input.toLowerCase()))); return (
{labels.map(label => ( handleRemove(label) : undefined} /> ))} {labels.length === 0 && !loading && ( No labels )} {canEdit && (
Add label
setInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); void handleAdd(input); } }} placeholder="prod" className="h-7 text-xs font-mono" disabled={busy} autoFocus />
{suggestions.length > 0 && (
Existing
{suggestions.slice(0, 12).map(s => ( ))}
)}

Labels group nodes for fleet-wide blueprints. Use lowercase letters, digits, dot, dash, underscore.

)}
); }