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; 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([]); const [targetId, setTargetId] = useState(""); const [state, setState] = useState(null); const [draft, setDraft] = useState(null); const [viewOptions, setViewOptions] = useState([]); const [surfaceOptions, setSurfaceOptions] = useState([]); 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(null); const [previewDraftKey, setPreviewDraftKey] = useState(""); const [resetImpactPreview, setResetImpactPreview] = useState(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 { 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 ( <> } > {needsTarget && ( void selectTarget(value)} placeholder={`Select ${scopeType}`} searchPlaceholder={`Search ${scopeType}s...`} disabled={loading || busy || targets.length === 0} /> )} {state && draft && ( <>
{BOOLEAN_FIELDS.map((field) => (
{field.label} {field.description} Effective: {effectiveLabel(state.effective_policy, field.id)}. ( 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`} />
))}
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 && ( setDraft({ ...draft, allowedViewIds: values })} provider={viewProvider} createCustomOption={(value) => customReference(value, parentViewIds)} placeholder="Add View" searchPlaceholder="Search Views or enter an ID..." disabled={!canWrite || busy} /> )} 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 && ( setDraft({ ...draft, visibleSurfaceIds: values })} provider={surfaceProvider} createCustomOption={(value) => customReference(value, parentSurfaceIds)} placeholder="Add surface" searchPlaceholder="Search surfaces or enter an ID..." disabled={!canWrite || busy} /> )}
Local override}> Allowed Views}>{ceilingLabel(state.effective_policy.allowed_view_ids)} Visible surfaces}>{ceilingLabel(state.effective_policy.visible_surface_ids)} Policy path}>{state.source_path.length ? state.source_path.map((step) => `${step.scope_type}${step.scope_id ? `:${step.scope_id}` : ""}`).join(" -> ") : "Platform defaults"} {impactPreview && ( Preview}>{impactPreview.preview_id} Draft state}> Newly allowed}>{impactPreview.counts.newly_allowed} Newly denied}>{impactPreview.counts.newly_denied} Unchanged}>{impactPreview.counts.unchanged} Indeterminate}>{impactPreview.counts.indeterminate} Risk}> Coverage}>{impactPreview.populations.map((population) => `${population.provider_id}: ${population.state} (${population.returned}${population.total_available == null ? "" : `/${population.total_available}`})${population.explanation ? ` - ${population.explanation}` : ""}`).join("; ")} {impactPreview.details_hidden && Details}>{impactPreview.details_explanation || "Subject details are hidden by policy."}} {impactPreview.effects.length > 0 && (

Changed subjects

    {impactPreview.effects.filter((effect) => effect.category !== "unchanged").slice(0, 20).map((effect) => (
  • {effect.category.replaceAll("_", " ")}: {effect.subject.label || effect.subject.resource_id} - {effect.subject.action} ({effect.rule})
  • ))}
{impactPreview.effects.filter((effect) => effect.category !== "unchanged").length > 20 &&

Only the first 20 changed subjects are shown; aggregate counts cover the complete returned population.

}
)}
)} )}
void resetPolicy()} onCancel={() => { setConfirmReset(false); setResetImpactPreview(null); }} /> ); } async function loadTargets(settings: ApiSettings, scope: ViewPolicyScope): Promise { 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 }; }