655 lines
25 KiB
TypeScript
655 lines
25 KiB
TypeScript
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<void>) | undefined;
|
|
type CreateCapabilitySetHandler = (payload: CapabilitySetMutationPayload) => Promise<boolean | CapabilitySet | void | null> | boolean | CapabilitySet | void | null;
|
|
type UpdateCapabilitySetHandler = (
|
|
id: CapabilitySetId,
|
|
payload: CapabilitySetMutationPayload,
|
|
) => Promise<boolean | CapabilitySet | void | null> | boolean | CapabilitySet | void | null;
|
|
type DeleteCapabilitySetHandler = (id: CapabilitySetId) => Promise<boolean | void | null> | 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<CapabilitySetsSectionProps> = ({
|
|
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<CapabilityValue[]>([]);
|
|
const [capabilitySetFormError, setCapabilitySetFormError] = useState<string | null>(null);
|
|
|
|
const [editingCapabilitySetId, setEditingCapabilitySetId] = useState<CapabilitySetId | null>(null);
|
|
const [editCapabilitySetSlug, setEditCapabilitySetSlug] = useState('');
|
|
const [editCapabilitySetLabel, setEditCapabilitySetLabel] = useState('');
|
|
const [editCapabilitySetCapabilities, setEditCapabilitySetCapabilities] = useState<CapabilityValue[]>([]);
|
|
const [capabilitySetEditError, setCapabilitySetEditError] = useState<string | null>(null);
|
|
|
|
const capabilitySelectionOptions = useMemo<CapabilityDropdownOption[]>(() => (
|
|
(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<CapabilityValue, string>();
|
|
capabilitySelectionOptions.forEach(({ value, label }) => {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
map.set(value, label || String(value));
|
|
});
|
|
return map;
|
|
}, [capabilitySelectionOptions]);
|
|
|
|
const capabilityOrder = useMemo(() => {
|
|
const order = new Map<CapabilityValue, number>();
|
|
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<HTMLFormElement>) => {
|
|
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<HTMLFormElement>) => {
|
|
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 (
|
|
<div className="settings-section">
|
|
<div className="settings-actions">
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={handleCapabilitySetsRefresh}
|
|
disabled={capabilitySetsLoading}
|
|
>
|
|
{capabilitySetsLoading ? 'Refreshing…' : 'Refresh'}
|
|
</button>
|
|
</div>
|
|
|
|
<p>
|
|
Capability sets bundle permissions that you can assign to API tokens and user memberships.
|
|
</p>
|
|
|
|
<form className="settings-form" onSubmit={handleCreateCapabilitySetSubmit}>
|
|
<div className="settings-form__field">
|
|
<label htmlFor="capability-set-slug">Slug</label>
|
|
<input
|
|
id="capability-set-slug"
|
|
type="text"
|
|
value={newCapabilitySetSlug}
|
|
onChange={(event) => setNewCapabilitySetSlug(event.target.value)}
|
|
placeholder="e.g. api_readonly"
|
|
disabled={creatingCapabilitySet || capabilitySetsLoading}
|
|
/>
|
|
<small>Leave blank to generate a slug automatically.</small>
|
|
</div>
|
|
{supportsCapabilitySetLabels ? (
|
|
<div className="settings-form__field">
|
|
<label htmlFor="capability-set-label">Label</label>
|
|
<input
|
|
id="capability-set-label"
|
|
type="text"
|
|
value={newCapabilitySetLabel}
|
|
onChange={(event) => setNewCapabilitySetLabel(event.target.value)}
|
|
placeholder="Friendly name (optional)"
|
|
disabled={creatingCapabilitySet || capabilitySetsLoading}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
<div className="settings-form__field settings-form__field--full">
|
|
<label htmlFor="capability-set-capabilities">Capabilities</label>
|
|
<div className="settings-capability-picker" id="capability-set-capabilities">
|
|
<CapabilityDropdown
|
|
id="capability-set-capabilities"
|
|
options={capabilitySelectionOptions}
|
|
selectedValues={newCapabilitySetCapabilities}
|
|
onSelect={handleAddCapabilityToNewSet}
|
|
onDeselect={handleRemoveCapabilityFromNewSet}
|
|
formatLabel={formatCapabilityLabel}
|
|
disabled={
|
|
creatingCapabilitySet
|
|
|| capabilitySetsLoading
|
|
|| capabilitiesLoading
|
|
|| !capabilitySelectionOptions.length
|
|
}
|
|
loading={capabilitiesLoading}
|
|
summaryLabel="capabilities"
|
|
/>
|
|
{newCapabilitySetCapabilities.length ? (
|
|
<div className="settings-capability-picker__chips">
|
|
{newCapabilitySetCapabilities.map((value) => (
|
|
<span key={value} className="badge tag-chip">
|
|
<span className="tag-chip__label">{formatCapabilityLabel(value)}</span>
|
|
<button
|
|
type="button"
|
|
className="tag-chip__remove"
|
|
onClick={() => handleRemoveCapabilityFromNewSet(value)}
|
|
aria-label={`Remove capability ${formatCapabilityLabel(value)}`}
|
|
>
|
|
<IconX className="icon-inline" aria-hidden="true" />
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="settings-capability-picker__placeholder">No capabilities selected.</span>
|
|
)}
|
|
{capabilitiesLoading ? (
|
|
<small>Loading capabilities…</small>
|
|
) : null}
|
|
{!capabilitiesLoading && !capabilitySelectionOptions.length ? (
|
|
<small>No capabilities available.</small>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div className="settings-form__actions">
|
|
<button
|
|
type="submit"
|
|
disabled={
|
|
creatingCapabilitySet
|
|
|| capabilitySetsLoading
|
|
|| capabilitiesLoading
|
|
|| !capabilitySelectionOptions.length
|
|
}
|
|
>
|
|
{creatingCapabilitySet ? 'Creating…' : 'Create capability set'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{capabilitySetFormError ? (
|
|
<p className="settings-form__error">{capabilitySetFormError}</p>
|
|
) : null}
|
|
{capabilitySetsLoading && !hasCapabilitySets ? (
|
|
<p className="settings-empty">Loading capability sets…</p>
|
|
) : null}
|
|
|
|
{!capabilitySetsLoading && !hasCapabilitySets ? (
|
|
<p className="settings-empty">No capability sets yet.</p>
|
|
) : null}
|
|
|
|
{hasCapabilitySets ? (
|
|
<table className="settings-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Slug</th>
|
|
{supportsCapabilitySetLabels ? <th scope="col">Label</th> : null}
|
|
<th scope="col">Capabilities</th>
|
|
<th scope="col">System</th>
|
|
<th scope="col">Version</th>
|
|
<th scope="col">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{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 (
|
|
<React.Fragment key={set.id}>
|
|
<tr className={isSystem ? 'is-system' : undefined}>
|
|
<td>{set.slug || '—'}</td>
|
|
{supportsCapabilitySetLabels ? (
|
|
<td>{set.label || '—'}</td>
|
|
) : null}
|
|
<td>
|
|
{capabilityList.length
|
|
? capabilityList.map((value) => formatCapabilityLabel(value)).join(', ')
|
|
: '—'}
|
|
</td>
|
|
<td>{isSystem ? 'Yes' : 'No'}</td>
|
|
<td>{Number.isFinite(set.cap_version) ? Number(set.cap_version) : '—'}</td>
|
|
<td className="settings-table__actions">
|
|
{isSystem ? (
|
|
<span className="settings-status">System set</span>
|
|
) : (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={() => handleStartEditCapabilitySet(set)}
|
|
disabled={
|
|
saving
|
|
|| deleting
|
|
|| capabilitySetsLoading
|
|
}
|
|
>
|
|
{isEditing ? 'Editing…' : 'Edit'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="danger"
|
|
onClick={() => handleDeleteCapabilitySet(set)}
|
|
disabled={deleting || saving || capabilitySetsLoading}
|
|
>
|
|
{deleting ? 'Deleting…' : 'Delete'}
|
|
</button>
|
|
</>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
{isEditing ? (
|
|
<tr className="settings-table__edit-row">
|
|
<td colSpan={columnCount}>
|
|
<form className="settings-form" onSubmit={handleUpdateCapabilitySetSubmit}>
|
|
<div className="settings-form__field">
|
|
<label htmlFor="edit-capability-set-slug">Slug</label>
|
|
<input
|
|
id="edit-capability-set-slug"
|
|
type="text"
|
|
value={editCapabilitySetSlug}
|
|
onChange={(event) => setEditCapabilitySetSlug(event.target.value)}
|
|
disabled={saving || capabilitySetsLoading}
|
|
/>
|
|
</div>
|
|
{supportsCapabilitySetLabels ? (
|
|
<div className="settings-form__field">
|
|
<label htmlFor="edit-capability-set-label">Label</label>
|
|
<input
|
|
id="edit-capability-set-label"
|
|
type="text"
|
|
value={editCapabilitySetLabel}
|
|
onChange={(event) => setEditCapabilitySetLabel(event.target.value)}
|
|
disabled={saving || capabilitySetsLoading}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
<div className="settings-form__field settings-form__field--full">
|
|
<label htmlFor={`edit-capability-set-${set.id}-capabilities`}>
|
|
Capabilities
|
|
</label>
|
|
<div
|
|
className="settings-capability-picker"
|
|
id={`edit-capability-set-${set.id}-capabilities`}
|
|
>
|
|
<CapabilityDropdown
|
|
id={`edit-capability-set-${set.id}-capabilities`}
|
|
options={capabilitySelectionOptions}
|
|
selectedValues={editCapabilitySetCapabilities}
|
|
onSelect={handleAddCapabilityToEditSet}
|
|
onDeselect={handleRemoveCapabilityFromEditSet}
|
|
formatLabel={formatCapabilityLabel}
|
|
disabled={
|
|
saving
|
|
|| capabilitySetsLoading
|
|
|| capabilitiesLoading
|
|
|| !capabilitySelectionOptions.length
|
|
}
|
|
loading={capabilitiesLoading}
|
|
summaryLabel="capabilities"
|
|
/>
|
|
{editCapabilitySetCapabilities.length ? (
|
|
<div className="settings-capability-picker__chips">
|
|
{editCapabilitySetCapabilities.map((value) => (
|
|
<span key={value} className="badge tag-chip">
|
|
<span className="tag-chip__label">{formatCapabilityLabel(value)}</span>
|
|
<button
|
|
type="button"
|
|
className="tag-chip__remove"
|
|
onClick={() => handleRemoveCapabilityFromEditSet(value)}
|
|
aria-label={`Remove capability ${formatCapabilityLabel(value)}`}
|
|
disabled={saving}
|
|
>
|
|
<IconX className="icon-inline" aria-hidden="true" />
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="settings-capability-picker__placeholder">
|
|
No capabilities selected.
|
|
</span>
|
|
)}
|
|
{capabilitiesLoading ? (
|
|
<small>Loading capabilities…</small>
|
|
) : null}
|
|
{!capabilitiesLoading && !capabilitySelectionOptions.length ? (
|
|
<small>No capabilities available.</small>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div className="settings-form__actions">
|
|
<button
|
|
type="submit"
|
|
disabled={
|
|
saving
|
|
|| capabilitySetsLoading
|
|
|| capabilitiesLoading
|
|
|| !capabilitySelectionOptions.length
|
|
}
|
|
>
|
|
{saving ? 'Saving…' : 'Save changes'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="secondary"
|
|
onClick={handleCancelEditCapabilitySet}
|
|
disabled={saving}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{capabilitySetEditError ? (
|
|
<p className="settings-form__error">{capabilitySetEditError}</p>
|
|
) : null}
|
|
</td>
|
|
</tr>
|
|
) : null}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
) : null}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const CAPABILITY_SETS_SECTION: SettingsSectionConfig = {
|
|
id: 'capabilitySets',
|
|
label: 'Capability sets',
|
|
component: CapabilitySetsSection,
|
|
};
|
|
|
|
export default CapabilitySetsSection;
|