import React, { FormEvent, useCallback, useEffect, useMemo, useState, } from 'react'; import type { SettingsSectionConfig } from '../SettingsModal'; import { IconX } from '../../ui/icons'; import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown'; import { isPlainObject } from '../../utils/typeGuards'; type CapabilityValue = string | number; type CapabilitySetId = string | number; interface CapabilitySet { id: CapabilitySetId; slug?: string; label?: string; capabilities?: CapabilityValue[] | null; is_system?: boolean; cap_version?: number | null; } interface CapabilitySetMutationPayload { slug?: string; label?: string; capabilities: CapabilityValue[]; } type RefreshHandler = (() => void | Promise) | undefined; type CreateCapabilitySetHandler = (payload: CapabilitySetMutationPayload) => Promise | boolean | CapabilitySet | void | null; type UpdateCapabilitySetHandler = ( id: CapabilitySetId, payload: CapabilitySetMutationPayload, ) => Promise | boolean | CapabilitySet | void | null; type DeleteCapabilitySetHandler = (id: CapabilitySetId) => Promise | boolean | void | null; interface CapabilitySetsSectionProps { capabilitySets?: CapabilitySet[]; capabilitySetsLoading?: boolean; creatingCapabilitySet?: boolean; savingCapabilitySetId?: CapabilitySetId | null; deletingCapabilitySetId?: CapabilitySetId | null; supportsCapabilitySetLabels?: boolean; capabilities?: CapabilityValue[]; capabilitiesLoading?: boolean; onRefreshCapabilitySets?: RefreshHandler; onRefreshCapabilities?: RefreshHandler; onRefresh?: RefreshHandler; onCreateCapabilitySet?: CreateCapabilitySetHandler; onUpdateCapabilitySet?: UpdateCapabilitySetHandler; onDeleteCapabilitySet?: DeleteCapabilitySetHandler; } type CapabilityOptionInput = CapabilityDropdownOption | CapabilityValue | null; const isCapabilityOption = (option: CapabilityOptionInput): option is CapabilityDropdownOption => isPlainObject(option); const resolveCapabilityValue = (option: CapabilityOptionInput): CapabilityValue | null => { if (option == null) { return null; } if (!isCapabilityOption(option)) { return option as CapabilityValue; } if (option.value != null) { return option.value as CapabilityValue; } if (option.id != null) { return option.id as CapabilityValue; } return null; }; const CapabilitySetsSection: React.FC = ({ capabilitySets = [], capabilitySetsLoading = false, creatingCapabilitySet = false, savingCapabilitySetId = null, deletingCapabilitySetId = null, supportsCapabilitySetLabels = false, capabilities = [], capabilitiesLoading = false, onRefreshCapabilitySets, onRefreshCapabilities, onRefresh, onCreateCapabilitySet, onUpdateCapabilitySet, onDeleteCapabilitySet, }) => { const [newCapabilitySetSlug, setNewCapabilitySetSlug] = useState(''); const [newCapabilitySetLabel, setNewCapabilitySetLabel] = useState(''); const [newCapabilitySetCapabilities, setNewCapabilitySetCapabilities] = useState([]); const [capabilitySetFormError, setCapabilitySetFormError] = useState(null); const [editingCapabilitySetId, setEditingCapabilitySetId] = useState(null); const [editCapabilitySetSlug, setEditCapabilitySetSlug] = useState(''); const [editCapabilitySetLabel, setEditCapabilitySetLabel] = useState(''); const [editCapabilitySetCapabilities, setEditCapabilitySetCapabilities] = useState([]); const [capabilitySetEditError, setCapabilitySetEditError] = useState(null); const capabilitySelectionOptions = useMemo(() => ( (capabilities ?? []).map((capability) => { const capabilityLabel = `${capability ?? ''}`; if (!capabilityLabel.includes(':')) { return { value: capability, label: capabilityLabel }; } const [namespace, action] = capabilityLabel.split(':'); if (!namespace || !action) { return { value: capability, label: capabilityLabel }; } const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`; const formattedAction = action.replace(/_/g, ' '); return { value: capability, label: `${formattedNamespace}: ${formattedAction}`, }; }) ), [capabilities]); const capabilityLabelMap = useMemo(() => { const map = new Map(); capabilitySelectionOptions.forEach(({ value, label }) => { if (value == null) { return; } map.set(value, label || String(value)); }); return map; }, [capabilitySelectionOptions]); const capabilityOrder = useMemo(() => { const order = new Map(); capabilitySelectionOptions.forEach((option, index) => { if (option.value == null) { return; } order.set(option.value, index); }); return order; }, [capabilitySelectionOptions]); const sortCapabilityValues = useCallback((values: CapabilityValue[] | null) => { if (!Array.isArray(values)) { return []; } return [...values].sort((a, b) => { const indexA = capabilityOrder.has(a) ? capabilityOrder.get(a)! : Number.MAX_SAFE_INTEGER; const indexB = capabilityOrder.has(b) ? capabilityOrder.get(b)! : Number.MAX_SAFE_INTEGER; if (indexA === indexB) { return String(a).localeCompare(String(b)); } return indexA - indexB; }); }, [capabilityOrder]); const formatCapabilityLabel = useCallback((value: CapabilityValue) => ( capabilityLabelMap.get(value) || String(value) ), [capabilityLabelMap]); const capabilitySetOptions = useMemo( () => capabilitySets.map((set) => ({ value: set.id, label: set.label || set.slug || set.id, capabilities: Array.isArray(set.capabilities) ? set.capabilities : [], isSystem: Boolean(set?.is_system), version: Number.isFinite(set?.cap_version) ? Number(set.cap_version) : null, })), [capabilitySets], ); const hasCapabilitySets = capabilitySetOptions.length > 0; const columnCount = supportsCapabilitySetLabels ? 6 : 5; const handleCapabilitySetsRefresh = useCallback(() => { if (onRefreshCapabilitySets) { onRefreshCapabilitySets(); } else { onRefresh?.(); } onRefreshCapabilities?.(); }, [onRefresh, onRefreshCapabilities, onRefreshCapabilitySets]); const handleAddCapabilityToNewSet = useCallback((option: CapabilityOptionInput) => { const value = resolveCapabilityValue(option); if (!value) { return; } setCapabilitySetFormError(null); setNewCapabilitySetCapabilities((previous) => { if (previous.includes(value)) { return previous; } return sortCapabilityValues([...previous, value]); }); }, [sortCapabilityValues]); const handleRemoveCapabilityFromNewSet = useCallback((value: CapabilityValue) => { setCapabilitySetFormError(null); setNewCapabilitySetCapabilities((previous) => previous.filter((item) => item !== value)); }, []); const handleCreateCapabilitySetSubmit = useCallback( async (event: FormEvent) => { event.preventDefault(); setCapabilitySetFormError(null); if (!Array.isArray(newCapabilitySetCapabilities) || newCapabilitySetCapabilities.length === 0) { setCapabilitySetFormError('Select at least one capability.'); return; } if (!capabilitySelectionOptions.length) { setCapabilitySetFormError('Capabilities are still loading.'); return; } const payload: CapabilitySetMutationPayload = { slug: newCapabilitySetSlug, label: newCapabilitySetLabel, capabilities: sortCapabilityValues(newCapabilitySetCapabilities), }; const result = await onCreateCapabilitySet?.(payload); if (result === false) { setCapabilitySetFormError('Failed to create capability set.'); return; } setNewCapabilitySetSlug(''); setNewCapabilitySetLabel(''); setNewCapabilitySetCapabilities([]); setCapabilitySetFormError(null); }, [ capabilitySelectionOptions, newCapabilitySetCapabilities, newCapabilitySetLabel, newCapabilitySetSlug, onCreateCapabilitySet, sortCapabilityValues, ], ); const handleStartEditCapabilitySet = useCallback((capabilitySet: CapabilitySet | null) => { if (!capabilitySet) { return; } setCapabilitySetEditError(null); setEditingCapabilitySetId(capabilitySet.id); setEditCapabilitySetSlug(capabilitySet.slug || ''); setEditCapabilitySetLabel(capabilitySet.label || ''); setEditCapabilitySetCapabilities( sortCapabilityValues(Array.isArray(capabilitySet.capabilities) ? capabilitySet.capabilities : []), ); }, [sortCapabilityValues]); const handleCancelEditCapabilitySet = useCallback(() => { setEditingCapabilitySetId(null); setEditCapabilitySetSlug(''); setEditCapabilitySetLabel(''); setEditCapabilitySetCapabilities([]); setCapabilitySetEditError(null); }, []); const handleAddCapabilityToEditSet = useCallback((option: CapabilityOptionInput) => { const value = resolveCapabilityValue(option); if (!value) { return; } setCapabilitySetEditError(null); setEditCapabilitySetCapabilities((previous) => { if (previous.includes(value)) { return previous; } return sortCapabilityValues([...previous, value]); }); }, [sortCapabilityValues]); const handleRemoveCapabilityFromEditSet = useCallback((value: CapabilityValue) => { setCapabilitySetEditError(null); setEditCapabilitySetCapabilities((previous) => previous.filter((item) => item !== value)); }, []); const handleUpdateCapabilitySetSubmit = useCallback( async (event: FormEvent) => { event.preventDefault(); if (!editingCapabilitySetId) { return; } if (!Array.isArray(editCapabilitySetCapabilities) || editCapabilitySetCapabilities.length === 0) { setCapabilitySetEditError('Select at least one capability.'); return; } const payload: CapabilitySetMutationPayload = { slug: editCapabilitySetSlug, label: editCapabilitySetLabel, capabilities: sortCapabilityValues(editCapabilitySetCapabilities), }; const result = await onUpdateCapabilitySet?.(editingCapabilitySetId, payload); if (result === false) { setCapabilitySetEditError('Failed to update capability set.'); return; } handleCancelEditCapabilitySet(); }, [ editCapabilitySetCapabilities, editCapabilitySetLabel, editCapabilitySetSlug, editingCapabilitySetId, handleCancelEditCapabilitySet, onUpdateCapabilitySet, sortCapabilityValues, ], ); const handleDeleteCapabilitySet = useCallback( async (capabilitySet: CapabilitySet | null) => { if (!capabilitySet?.id) { return; } const displayName = capabilitySet.slug || capabilitySet.label || capabilitySet.id; const confirmed = window.confirm(`Delete capability set "${displayName}"?`); if (!confirmed) { return; } const result = await onDeleteCapabilitySet?.(capabilitySet.id); if (result === false) { setCapabilitySetEditError('Failed to delete capability set.'); } }, [onDeleteCapabilitySet], ); useEffect(() => { if (!editingCapabilitySetId) { return; } const exists = capabilitySets.some((set) => set.id === editingCapabilitySetId); if (!exists) { handleCancelEditCapabilitySet(); } }, [capabilitySets, editingCapabilitySetId, handleCancelEditCapabilitySet]); return (

Capability sets bundle permissions that you can assign to API tokens and user memberships.

setNewCapabilitySetSlug(event.target.value)} placeholder="e.g. api_readonly" disabled={creatingCapabilitySet || capabilitySetsLoading} /> Leave blank to generate a slug automatically.
{supportsCapabilitySetLabels ? (
setNewCapabilitySetLabel(event.target.value)} placeholder="Friendly name (optional)" disabled={creatingCapabilitySet || capabilitySetsLoading} />
) : null}
{newCapabilitySetCapabilities.length ? (
{newCapabilitySetCapabilities.map((value) => ( {formatCapabilityLabel(value)} ))}
) : ( No capabilities selected. )} {capabilitiesLoading ? ( Loading capabilities… ) : null} {!capabilitiesLoading && !capabilitySelectionOptions.length ? ( No capabilities available. ) : null}
{capabilitySetFormError ? (

{capabilitySetFormError}

) : null} {capabilitySetsLoading && !hasCapabilitySets ? (

Loading capability sets…

) : null} {!capabilitySetsLoading && !hasCapabilitySets ? (

No capability sets yet.

) : null} {hasCapabilitySets ? ( {supportsCapabilitySetLabels ? : null} {capabilitySets.map((set) => { const isSystem = Boolean(set?.is_system); const isEditing = editingCapabilitySetId === set.id; const capabilityList = sortCapabilityValues( Array.isArray(set?.capabilities) ? set.capabilities : [], ); const saving = savingCapabilitySetId === set.id; const deleting = deletingCapabilitySetId === set.id; return ( {supportsCapabilitySetLabels ? ( ) : null} {isEditing ? ( ) : null} ); })}
SlugLabelCapabilities System Version Actions
{set.slug || '—'}{set.label || '—'} {capabilityList.length ? capabilityList.map((value) => formatCapabilityLabel(value)).join(', ') : '—'} {isSystem ? 'Yes' : 'No'} {Number.isFinite(set.cap_version) ? Number(set.cap_version) : '—'} {isSystem ? ( System set ) : ( <> )}
setEditCapabilitySetSlug(event.target.value)} disabled={saving || capabilitySetsLoading} />
{supportsCapabilitySetLabels ? (
setEditCapabilitySetLabel(event.target.value)} disabled={saving || capabilitySetsLoading} />
) : null}
{editCapabilitySetCapabilities.length ? (
{editCapabilitySetCapabilities.map((value) => ( {formatCapabilityLabel(value)} ))}
) : ( No capabilities selected. )} {capabilitiesLoading ? ( Loading capabilities… ) : null} {!capabilitiesLoading && !capabilitySelectionOptions.length ? ( No capabilities available. ) : null}
{capabilitySetEditError ? (

{capabilitySetEditError}

) : null}
) : null}
); }; export const CAPABILITY_SETS_SECTION: SettingsSectionConfig = { id: 'capabilitySets', label: 'Capability sets', component: CapabilitySetsSection, }; export default CapabilitySetsSection;