565 lines
23 KiB
TypeScript
565 lines
23 KiB
TypeScript
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
AdminPageLayout,
|
|
adminErrorMessage,
|
|
Button,
|
|
Card,
|
|
ConfirmDialog,
|
|
DocumentationHelpLink,
|
|
FormField,
|
|
ReferenceMultiSelect,
|
|
SearchableSelect,
|
|
SegmentedControl,
|
|
staticReferenceOptionProvider,
|
|
StatusBadge,
|
|
ToggleSwitch,
|
|
useUnsavedDraftGuard,
|
|
type ApiSettings,
|
|
type ReferenceOption,
|
|
type SearchableSelectOption
|
|
} from "@govoplan/core-webui";
|
|
import { RefreshCw, Save, ScanSearch, Trash2, Undo2 } from "lucide-react";
|
|
import { fetchGroupsDelta, fetchUsersDelta } from "../../api/adminTargets";
|
|
import {
|
|
deleteViewPolicy,
|
|
fetchViewPolicy,
|
|
fetchViewPolicyReferences,
|
|
previewViewPolicyImpact,
|
|
updateViewPolicy,
|
|
type EffectiveViewPolicy,
|
|
type PolicyImpactPreviewResponse,
|
|
type ViewPolicyItem,
|
|
type ViewPolicyScope,
|
|
type ViewPolicyScopeResponse
|
|
} from "../../api/viewPolicies";
|
|
|
|
type Props = {
|
|
settings: ApiSettings;
|
|
scopeType: ViewPolicyScope;
|
|
canWrite: boolean;
|
|
};
|
|
|
|
type Decision = "inherit" | "allow" | "block";
|
|
|
|
type Draft = {
|
|
allow_view: Decision;
|
|
allow_select: Decision;
|
|
allow_assign: Decision;
|
|
allow_edit: Decision;
|
|
allow_derive: Decision;
|
|
allow_workflow_activate: Decision;
|
|
limitViews: boolean;
|
|
allowedViewIds: string[];
|
|
limitSurfaces: boolean;
|
|
visibleSurfaceIds: string[];
|
|
};
|
|
|
|
type Target = SearchableSelectOption;
|
|
|
|
const BOOLEAN_FIELDS: Array<{
|
|
id: keyof Pick<Draft, "allow_view" | "allow_select" | "allow_assign" | "allow_edit" | "allow_derive" | "allow_workflow_activate">;
|
|
label: string;
|
|
description: string;
|
|
}> = [
|
|
{ id: "allow_view", label: "View", description: "Allow affected accounts to apply and use Views." },
|
|
{ id: "allow_select", label: "Select", description: "Allow affected accounts to choose among available Views." },
|
|
{ id: "allow_assign", label: "Assign", description: "Allow administrators at this scope to assign Views." },
|
|
{ id: "allow_edit", label: "Edit", description: "Allow View definitions to be edited at this scope." },
|
|
{ id: "allow_derive", label: "Derive", description: "Allow a new View to derive from an inherited definition." },
|
|
{ id: "allow_workflow_activate", label: "Workflow activation", description: "Allow workflows to activate a View for affected accounts." }
|
|
];
|
|
|
|
const DOCUMENTATION = {
|
|
contextId: "policy.view-governance",
|
|
documentationType: "admin" as const
|
|
};
|
|
|
|
const DECISION_OPTIONS = [
|
|
{ id: "inherit" as const, label: "Inherit" },
|
|
{ id: "allow" as const, label: "Allow" },
|
|
{ id: "block" as const, label: "Block" }
|
|
];
|
|
|
|
export default function ViewPoliciesPanel({ settings, scopeType, canWrite }: Props) {
|
|
const [targets, setTargets] = useState<Target[]>([]);
|
|
const [targetId, setTargetId] = useState("");
|
|
const [state, setState] = useState<ViewPolicyScopeResponse | null>(null);
|
|
const [draft, setDraft] = useState<Draft | null>(null);
|
|
const [viewOptions, setViewOptions] = useState<ReferenceOption[]>([]);
|
|
const [surfaceOptions, setSurfaceOptions] = useState<ReferenceOption[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
const [confirmReset, setConfirmReset] = useState(false);
|
|
const [impactPreview, setImpactPreview] = useState<PolicyImpactPreviewResponse | null>(null);
|
|
const [previewDraftKey, setPreviewDraftKey] = useState("");
|
|
const [resetImpactPreview, setResetImpactPreview] = useState<PolicyImpactPreviewResponse | null>(null);
|
|
|
|
const needsTarget = scopeType === "group" || scopeType === "user";
|
|
const parentViewIds = state?.parent_policy.allowed_view_ids;
|
|
const parentSurfaceIds = state?.parent_policy.visible_surface_ids;
|
|
const viewProvider = useMemo(
|
|
() => staticReferenceOptionProvider(optionsWithinCeiling(viewOptions, parentViewIds)),
|
|
[parentViewIds, viewOptions]
|
|
);
|
|
const surfaceProvider = useMemo(
|
|
() => staticReferenceOptionProvider(optionsWithinCeiling(surfaceOptions, parentSurfaceIds)),
|
|
[parentSurfaceIds, surfaceOptions]
|
|
);
|
|
const dirty = Boolean(
|
|
state
|
|
&& draft
|
|
&& stablePolicy(buildPolicy(draft))
|
|
!== stablePolicy(buildPolicy(draftFromPolicy(state.policy)))
|
|
);
|
|
const draftKey = draft ? stablePolicy(buildPolicy(draft)) : "";
|
|
const previewCurrent = Boolean(impactPreview && previewDraftKey === draftKey);
|
|
|
|
useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: discard });
|
|
|
|
useEffect(() => {
|
|
void initialize();
|
|
}, [scopeType, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
|
|
|
async function initialize() {
|
|
setLoading(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const [references, loadedTargets] = await Promise.all([
|
|
fetchViewPolicyReferences(settings),
|
|
loadTargets(settings, scopeType)
|
|
]);
|
|
setViewOptions(references.views.map((view) => ({
|
|
value: view.id,
|
|
label: view.name,
|
|
description: view.scope_type ? `${view.scope_type} View` : "View",
|
|
searchText: `${view.name} ${view.id}`
|
|
})));
|
|
setSurfaceOptions(references.surfaces.map((surface) => ({
|
|
value: surface.id,
|
|
label: surface.label || surface.id,
|
|
description: `${surface.module_id} - ${surface.kind}`,
|
|
searchText: `${surface.id} ${surface.module_id} ${surface.label}`
|
|
})));
|
|
setTargets(loadedTargets);
|
|
const nextTarget = needsTarget
|
|
? (loadedTargets.some((target) => target.value === targetId) ? targetId : loadedTargets[0]?.value ?? "")
|
|
: "";
|
|
setTargetId(nextTarget);
|
|
if (!needsTarget || nextTarget) await load(nextTarget, false);
|
|
else {
|
|
setState(null);
|
|
setDraft(null);
|
|
}
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
setState(null);
|
|
setDraft(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function load(nextTargetId = targetId, manageLoading = true) {
|
|
if (needsTarget && !nextTargetId) return;
|
|
if (manageLoading) setLoading(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const loaded = await fetchViewPolicy(settings, scopeType, nextTargetId || null);
|
|
setState(loaded);
|
|
setDraft(draftFromPolicy(loaded.policy));
|
|
setImpactPreview(null);
|
|
setPreviewDraftKey("");
|
|
setResetImpactPreview(null);
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
if (manageLoading) setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function selectTarget(nextTargetId: string) {
|
|
if (!nextTargetId || nextTargetId === targetId) return;
|
|
setTargetId(nextTargetId);
|
|
await load(nextTargetId);
|
|
}
|
|
|
|
function discard() {
|
|
if (state) setDraft(draftFromPolicy(state.policy));
|
|
setImpactPreview(null);
|
|
setPreviewDraftKey("");
|
|
setResetImpactPreview(null);
|
|
setError("");
|
|
setSuccess("");
|
|
}
|
|
|
|
async function previewImpact() {
|
|
if (!draft || !state || !dirty) return;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const policy = buildPolicy(draft);
|
|
const preview = await previewViewPolicyImpact(
|
|
settings,
|
|
scopeType,
|
|
targetId || null,
|
|
policy,
|
|
{
|
|
viewIds: viewOptions.map((option) => option.value),
|
|
surfaceIds: surfaceOptions.map((option) => option.value)
|
|
}
|
|
);
|
|
setImpactPreview(preview);
|
|
setPreviewDraftKey(stablePolicy(policy));
|
|
setSuccess("Policy impact preview completed without saving the draft.");
|
|
} catch (err) {
|
|
setImpactPreview(null);
|
|
setPreviewDraftKey("");
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function save(): Promise<boolean> {
|
|
if (!draft || !state || !dirty) return true;
|
|
if (!previewCurrent) {
|
|
setError("Preview the current policy draft before saving it.");
|
|
return false;
|
|
}
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const loaded = await updateViewPolicy(
|
|
settings,
|
|
scopeType,
|
|
targetId || null,
|
|
buildPolicy(draft),
|
|
impactPreview
|
|
);
|
|
setState(loaded);
|
|
setDraft(draftFromPolicy(loaded.policy));
|
|
setImpactPreview(null);
|
|
setPreviewDraftKey("");
|
|
setSuccess("View policy saved.");
|
|
return true;
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
return false;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function prepareResetPolicy() {
|
|
if (!state?.id) return;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const preview = await previewViewPolicyImpact(
|
|
settings,
|
|
scopeType,
|
|
targetId || null,
|
|
{},
|
|
{
|
|
viewIds: viewOptions.map((option) => option.value),
|
|
surfaceIds: surfaceOptions.map((option) => option.value)
|
|
}
|
|
);
|
|
setResetImpactPreview(preview);
|
|
setConfirmReset(true);
|
|
setSuccess("Inherited-policy impact preview completed without removing the override.");
|
|
} catch (err) {
|
|
setResetImpactPreview(null);
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function resetPolicy() {
|
|
if (!resetImpactPreview) return;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const loaded = await deleteViewPolicy(
|
|
settings,
|
|
scopeType,
|
|
targetId || null,
|
|
resetImpactPreview
|
|
);
|
|
setState(loaded);
|
|
setDraft(draftFromPolicy(loaded.policy));
|
|
setImpactPreview(null);
|
|
setPreviewDraftKey("");
|
|
setResetImpactPreview(null);
|
|
setSuccess("Local View policy removed; inherited policy now applies.");
|
|
setConfirmReset(false);
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const scopeLabel = scopeType === "system" ? "System" : scopeType === "tenant" ? "Tenant" : scopeType === "group" ? "Group" : "User";
|
|
|
|
return (
|
|
<>
|
|
<AdminPageLayout
|
|
title={`${scopeLabel} View policy`}
|
|
description="Control which Views and surfaces are available, forced by assignment, selectable, editable, derivable, or workflow-activatable at this scope."
|
|
loading={loading}
|
|
error={error}
|
|
success={success}
|
|
actions={
|
|
<>
|
|
<Button title="Reload saved View policy" aria-label="Reload saved View policy" onClick={() => void load()} disabled={loading || busy || (needsTarget && !targetId)}>
|
|
<RefreshCw size={16} />
|
|
</Button>
|
|
<Button onClick={discard} disabled={!dirty || busy}><Undo2 size={16} /> Discard</Button>
|
|
<Button onClick={() => void prepareResetPolicy()} disabled={!canWrite || !state?.id || busy}><Trash2 size={16} /> Use inherited</Button>
|
|
<Button helpContextId="policy.impact-preview.action.preview" onClick={() => void previewImpact()} disabled={!canWrite || !dirty || busy}><ScanSearch size={16} /> Preview impact</Button>
|
|
<Button variant="primary" onClick={() => void save()} disabled={!canWrite || !dirty || busy || !previewCurrent} disabledReason={dirty && !previewCurrent ? "Preview the current draft before saving." : undefined}><Save size={16} /> {busy ? "Working..." : "Save"}</Button>
|
|
<DocumentationHelpLink reference={DOCUMENTATION} label="Open View policy documentation" />
|
|
</>
|
|
}
|
|
>
|
|
{needsTarget && (
|
|
<FormField label={scopeType === "group" ? "Group" : "User"} documentation={DOCUMENTATION}>
|
|
<SearchableSelect
|
|
value={targetId}
|
|
options={targets}
|
|
onChange={(value) => void selectTarget(value)}
|
|
placeholder={`Select ${scopeType}`}
|
|
searchPlaceholder={`Search ${scopeType}s...`}
|
|
disabled={loading || busy || targets.length === 0}
|
|
/>
|
|
</FormField>
|
|
)}
|
|
|
|
{state && draft && (
|
|
<>
|
|
<Card title="Actions">
|
|
<div className="settings-list">
|
|
{BOOLEAN_FIELDS.map((field) => (
|
|
<div className="admin-tenant-assignment-row" key={field.id}>
|
|
<span>
|
|
<strong>{field.label}</strong>
|
|
<small>{field.description} Effective: {effectiveLabel(state.effective_policy, field.id)}.</small>
|
|
</span>
|
|
<SegmentedControl
|
|
options={DECISION_OPTIONS.map((option) => (
|
|
option.id === "allow" && state.parent_policy[field.id] === false
|
|
? {
|
|
...option,
|
|
disabled: true,
|
|
title: "A parent policy blocks this action."
|
|
}
|
|
: option
|
|
))}
|
|
value={draft[field.id]}
|
|
onChange={(value) => setDraft({ ...draft, [field.id]: value })}
|
|
role="group"
|
|
size="equal"
|
|
width="fill"
|
|
disabled={!canWrite || busy}
|
|
ariaLabel={`${field.label} policy`}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="Availability ceilings">
|
|
<div className="settings-list">
|
|
<ToggleSwitch
|
|
label="Limit available Views"
|
|
checked={draft.limitViews}
|
|
onChange={(checked) => setDraft({ ...draft, limitViews: checked, allowedViewIds: checked ? draft.allowedViewIds : [] })}
|
|
disabled={!canWrite || busy}
|
|
help="A lower scope may narrow this list but cannot add Views excluded by an ancestor."
|
|
/>
|
|
{draft.limitViews && (
|
|
<FormField label="Available Views" documentation={DOCUMENTATION}>
|
|
<ReferenceMultiSelect
|
|
values={draft.allowedViewIds}
|
|
onChange={(values) => setDraft({ ...draft, allowedViewIds: values })}
|
|
provider={viewProvider}
|
|
createCustomOption={(value) => customReference(value, parentViewIds)}
|
|
placeholder="Add View"
|
|
searchPlaceholder="Search Views or enter an ID..."
|
|
disabled={!canWrite || busy}
|
|
/>
|
|
</FormField>
|
|
)}
|
|
<ToggleSwitch
|
|
label="Limit visible surfaces"
|
|
checked={draft.limitSurfaces}
|
|
onChange={(checked) => setDraft({ ...draft, limitSurfaces: checked, visibleSurfaceIds: checked ? draft.visibleSurfaceIds : [] })}
|
|
disabled={!canWrite || busy}
|
|
help="The effective View may only expose surfaces retained by every ancestor policy."
|
|
/>
|
|
{draft.limitSurfaces && (
|
|
<FormField label="Visible surfaces" documentation={DOCUMENTATION}>
|
|
<ReferenceMultiSelect
|
|
values={draft.visibleSurfaceIds}
|
|
onChange={(values) => setDraft({ ...draft, visibleSurfaceIds: values })}
|
|
provider={surfaceProvider}
|
|
createCustomOption={(value) => customReference(value, parentSurfaceIds)}
|
|
placeholder="Add surface"
|
|
searchPlaceholder="Search surfaces or enter an ID..."
|
|
disabled={!canWrite || busy}
|
|
/>
|
|
</FormField>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="Effective policy and provenance">
|
|
<DescriptionList>
|
|
<DescriptionItem term={<>Local override</>}><StatusBadge status={state.id ? "info" : "neutral"} label={state.id ? `Revision ${state.revision}` : "Inherited"} /></DescriptionItem>
|
|
<DescriptionItem term={<>Allowed Views</>}>{ceilingLabel(state.effective_policy.allowed_view_ids)}</DescriptionItem>
|
|
<DescriptionItem term={<>Visible surfaces</>}>{ceilingLabel(state.effective_policy.visible_surface_ids)}</DescriptionItem>
|
|
<DescriptionItem term={<>Policy path</>}>{state.source_path.length ? state.source_path.map((step) => `${step.scope_type}${step.scope_id ? `:${step.scope_id}` : ""}`).join(" -> ") : "Platform defaults"}</DescriptionItem>
|
|
</DescriptionList>
|
|
</Card>
|
|
|
|
{impactPreview && (
|
|
<Card title="Policy impact preview">
|
|
<DescriptionList>
|
|
<DescriptionItem term={<>Preview</>}><code>{impactPreview.preview_id}</code></DescriptionItem>
|
|
<DescriptionItem term={<>Draft state</>}><StatusBadge status={previewCurrent ? "success" : "warning"} label={previewCurrent ? "Current" : "Outdated"} /></DescriptionItem>
|
|
<DescriptionItem term={<>Newly allowed</>}>{impactPreview.counts.newly_allowed}</DescriptionItem>
|
|
<DescriptionItem term={<>Newly denied</>}>{impactPreview.counts.newly_denied}</DescriptionItem>
|
|
<DescriptionItem term={<>Unchanged</>}>{impactPreview.counts.unchanged}</DescriptionItem>
|
|
<DescriptionItem term={<>Indeterminate</>}>{impactPreview.counts.indeterminate}</DescriptionItem>
|
|
<DescriptionItem term={<>Risk</>}><StatusBadge status={impactPreview.high_impact ? "warning" : "neutral"} label={impactPreview.high_impact ? "High impact - recent login required" : "Bounded change"} /></DescriptionItem>
|
|
<DescriptionItem term={<>Coverage</>}>{impactPreview.populations.map((population) => `${population.provider_id}: ${population.state} (${population.returned}${population.total_available == null ? "" : `/${population.total_available}`})${population.explanation ? ` - ${population.explanation}` : ""}`).join("; ")}</DescriptionItem>
|
|
{impactPreview.details_hidden && <DescriptionItem term={<>Details</>}>{impactPreview.details_explanation || "Subject details are hidden by policy."}</DescriptionItem>}
|
|
</DescriptionList>
|
|
{impactPreview.effects.length > 0 && (
|
|
<div help-context-id="policy.impact-preview.results">
|
|
<h4>Changed subjects</h4>
|
|
<ul>
|
|
{impactPreview.effects.filter((effect) => effect.category !== "unchanged").slice(0, 20).map((effect) => (
|
|
<li key={`${effect.subject.module_id}:${effect.subject.resource_type}:${effect.subject.resource_id}:${effect.subject.action}`}>
|
|
<strong>{effect.category.replaceAll("_", " ")}</strong>: {effect.subject.label || effect.subject.resource_id} - {effect.subject.action} ({effect.rule})
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{impactPreview.effects.filter((effect) => effect.category !== "unchanged").length > 20 && <p>Only the first 20 changed subjects are shown; aggregate counts cover the complete returned population.</p>}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
</AdminPageLayout>
|
|
|
|
<ConfirmDialog
|
|
open={confirmReset}
|
|
title="Use inherited View policy?"
|
|
message={resetImpactPreview ? `The local override will be removed. The bounded preview found ${resetImpactPreview.counts.newly_allowed} newly allowed, ${resetImpactPreview.counts.newly_denied} newly denied, and ${resetImpactPreview.counts.indeterminate} indeterminate effects. All restrictions inherited from higher scopes continue to apply.` : "Previewing inherited-policy impact..."}
|
|
confirmLabel="Use inherited policy"
|
|
busy={busy}
|
|
onConfirm={() => void resetPolicy()}
|
|
onCancel={() => {
|
|
setConfirmReset(false);
|
|
setResetImpactPreview(null);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
async function loadTargets(settings: ApiSettings, scope: ViewPolicyScope): Promise<Target[]> {
|
|
if (scope === "user") {
|
|
const response = await fetchUsersDelta(settings, { limit: 200 });
|
|
return response.users.map((user) => ({
|
|
value: user.id,
|
|
label: user.display_name || user.email,
|
|
description: user.display_name ? user.email : undefined,
|
|
searchText: `${user.display_name ?? ""} ${user.email}`
|
|
}));
|
|
}
|
|
if (scope === "group") {
|
|
const response = await fetchGroupsDelta(settings, { limit: 200 });
|
|
return response.groups.map((group) => ({
|
|
value: group.id,
|
|
label: group.name,
|
|
description: group.slug,
|
|
searchText: `${group.name} ${group.slug}`
|
|
}));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function draftFromPolicy(policy: ViewPolicyItem): Draft {
|
|
return {
|
|
allow_view: decision(policy.allow_view),
|
|
allow_select: decision(policy.allow_select),
|
|
allow_assign: decision(policy.allow_assign),
|
|
allow_edit: decision(policy.allow_edit),
|
|
allow_derive: decision(policy.allow_derive),
|
|
allow_workflow_activate: decision(policy.allow_workflow_activate),
|
|
limitViews: Array.isArray(policy.allowed_view_ids),
|
|
allowedViewIds: [...(policy.allowed_view_ids ?? [])].sort(),
|
|
limitSurfaces: Array.isArray(policy.visible_surface_ids),
|
|
visibleSurfaceIds: [...(policy.visible_surface_ids ?? [])].sort()
|
|
};
|
|
}
|
|
|
|
function buildPolicy(draft: Draft): ViewPolicyItem {
|
|
const policy: ViewPolicyItem = {};
|
|
for (const field of BOOLEAN_FIELDS) {
|
|
const value = draft[field.id];
|
|
if (value !== "inherit") policy[field.id] = value === "allow";
|
|
}
|
|
if (draft.limitViews) policy.allowed_view_ids = [...new Set(draft.allowedViewIds)].sort();
|
|
if (draft.limitSurfaces) policy.visible_surface_ids = [...new Set(draft.visibleSurfaceIds)].sort();
|
|
return policy;
|
|
}
|
|
|
|
function decision(value: boolean | undefined): Decision {
|
|
return value === undefined ? "inherit" : value ? "allow" : "block";
|
|
}
|
|
|
|
function stablePolicy(policy: ViewPolicyItem): string {
|
|
return JSON.stringify(Object.fromEntries(Object.entries(policy).sort(([left], [right]) => left.localeCompare(right))));
|
|
}
|
|
|
|
function effectiveLabel(policy: EffectiveViewPolicy, field: keyof EffectiveViewPolicy): string {
|
|
return policy[field] === true ? "allowed" : "blocked";
|
|
}
|
|
|
|
function ceilingLabel(values: string[] | null | undefined): string {
|
|
return values == null ? "Unrestricted by ID" : values.length ? `${values.length} entries` : "None";
|
|
}
|
|
|
|
function optionsWithinCeiling(
|
|
options: ReferenceOption[],
|
|
ceiling: string[] | null | undefined
|
|
): ReferenceOption[] {
|
|
if (!Array.isArray(ceiling)) return options;
|
|
const allowed = new Set(ceiling);
|
|
return options.filter((option) => allowed.has(option.value));
|
|
}
|
|
|
|
function customReference(
|
|
value: string,
|
|
ceiling: string[] | null | undefined
|
|
): ReferenceOption | null {
|
|
const clean = value.trim();
|
|
if (!clean || (Array.isArray(ceiling) && !ceiling.includes(clean))) return null;
|
|
return { value: clean, label: clean, description: "Unresolved identifier", custom: true };
|
|
}
|