import { useEffect, useMemo, useRef, useState } from "react"; import { KeyRound, Pencil, Plus, Search, Trash2 } from "lucide-react"; import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { createUser, fetchGroupsDelta, fetchRolesDelta, fetchUserAccessExplanation, fetchUsersDelta, updateUser, type AccessRoleSourceItem, type FunctionFactExplanationItem, type GroupSummary, type RoleSummary, type UserAccessExplanationResponse, type UserAdminItem } from "../../api/admin"; import { Button } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { PasswordField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels } from "@govoplan/core-webui"; import { hasTenantWildcard, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; const emptyDraft = { email: "", displayName: "", password: "", passwordResetRequired: true, isActive: true, groupIds: [] as string[], roleIds: [] as string[] }; export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSuspend, canManageGroups, canAssignRoles, onAuthRefresh }: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;canManageGroups: boolean;canAssignRoles: boolean;onAuthRefresh: () => Promise;}) { const [users, setUsers] = useState([]); const [groups, setGroups] = useState([]); const [roles, setRoles] = useState([]); const usersRef = useRef([]); const groupsRef = useRef([]); const rolesRef = useRef([]); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [explaining, setExplaining] = useState(null); const [accessExplanation, setAccessExplanation] = useState(null); const [accessExplanationLoading, setAccessExplanationLoading] = useState(false); const [deactivating, setDeactivating] = useState(null); const [draft, setDraft] = useState(emptyDraft); const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft)); const [temporaryPassword, setTemporaryPassword] = useState<{email: string;password: string;} | null>(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const dirty = editing !== null && draftKey(draft) !== savedDraftKey; useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: closeEditor }); async function load() { setLoading(true); setError(""); try { const [nextUsers, nextGroups, nextRoles] = await Promise.all([ loadDeltaRows(usersRef.current, "access:users", getDeltaWatermark, setDeltaWatermark, (since) => fetchUsersDelta(settings, { since }), (response) => response.users, (user) => user.id, "access_user", sortUsers), loadDeltaRows(groupsRef.current, "access:groups", getDeltaWatermark, setDeltaWatermark, (since) => fetchGroupsDelta(settings, { since }), (response) => response.groups, (group) => group.id, "access_group", sortGroups), loadDeltaRows(rolesRef.current, "access:roles", getDeltaWatermark, setDeltaWatermark, (since) => fetchRolesDelta(settings, { since }), (response) => response.roles, (role) => role.id, "access_role", sortTenantRoles)] ); usersRef.current = nextUsers; groupsRef.current = nextGroups; rolesRef.current = nextRoles; setUsers(nextUsers); setGroups(nextGroups); setRoles(nextRoles.filter((role) => role.is_assignable)); } catch (err) {setError(adminErrorMessage(err));} finally {setLoading(false);} } useEffect(() => { usersRef.current = []; groupsRef.current = []; rolesRef.current = []; resetDeltaWatermark(); void load(); }, [settings.accessToken, settings.apiBaseUrl, (auth.active_tenant ?? auth.tenant).id, resetDeltaWatermark]); function openCreate() { setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); setEditing("new"); setError(""); } function openEdit(user: UserAdminItem) { const nextDraft = { email: user.email, displayName: user.display_name || "", password: "", passwordResetRequired: user.password_reset_required, isActive: user.is_active, groupIds: user.groups.map((group) => group.id), roleIds: user.roles.map((role) => role.id) }; setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setEditing(user); setError(""); } function closeEditor() { setEditing(null); setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); } async function save(): Promise { setBusy(true); setError(""); try { if (editing === "new") { const response = await createUser(settings, { email: draft.email, display_name: draft.displayName || null, password: draft.password || null, password_reset_required: draft.passwordResetRequired, is_active: draft.isActive, group_ids: canManageGroups ? draft.groupIds : [], role_ids: canAssignRoles ? draft.roleIds : [] }); setSuccess(i18nMessage("i18n:govoplan-access.tenant_membership_created_for_value.2b8eeef6", { value0: response.user.email })); if (response.temporary_password) setTemporaryPassword({ email: response.user.email, password: response.temporary_password }); } else if (editing) { const payload: Parameters[2] = {}; if (canUpdate) payload.display_name = draft.displayName || null; if (canSuspend) payload.is_active = draft.isActive; if (canManageGroups) payload.group_ids = draft.groupIds; if (canAssignRoles) payload.role_ids = draft.roleIds; await updateUser(settings, editing.id, payload); setSuccess(i18nMessage("i18n:govoplan-access.access_updated_for_value.87f22245", { value0: editing.email })); if (editing.id === auth.user.id) await onAuthRefresh(); } setEditing(null); await load(); return true; } catch (err) {setError(adminErrorMessage(err));return false;} finally {setBusy(false);} } async function deactivate() { if (!deactivating) return; setBusy(true); setError(""); try { await updateUser(settings, deactivating.id, { is_active: false }); setSuccess(i18nMessage("i18n:govoplan-access.value_deactivated_in_this_tenant.871837c2", { value0: deactivating.email })); if (deactivating.id === auth.user.id) await onAuthRefresh(); setDeactivating(null); await load(); } catch (err) {setError(adminErrorMessage(err));} finally {setBusy(false);} } async function openAccessExplanation(user: UserAdminItem) { setExplaining(user); setAccessExplanation(null); setAccessExplanationLoading(true); setError(""); try { setAccessExplanation(await fetchUserAccessExplanation(settings, user.id)); } catch (err) { setError(adminErrorMessage(err)); setExplaining(null); } finally { setAccessExplanationLoading(false); } } const columns = useMemo[]>(() => [ { id: "user", header: "i18n:govoplan-access.user.9f8a2389", width: "minmax(230px, 1fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.display_name || ""} ${row.email}`, render: (row) =>
{row.display_name || row.email}
{row.email}
}, { id: "groups", header: "i18n:govoplan-access.groups.ae9629f4", width: 210, minWidth: 150, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => joinLabels(row.groups) }, { id: "roles", header: "i18n:govoplan-access.direct_roles.c4db7156", width: 220, minWidth: 150, maxWidth: 460, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => joinLabels(row.roles) }, { id: "scope_count", header: "i18n:govoplan-access.permissions.d06d5557", width: 110, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => hasTenantWildcard(row.effective_scopes) ? 999 : row.effective_scopes.length, render: (row) => hasTenantWildcard(row.effective_scopes) ? "i18n:govoplan-access.all.6a720856" : String(row.effective_scopes.length) }, { id: "status", header: "i18n:govoplan-access.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active && row.account_is_active ? "active" : "inactive", render: (row) => }, { id: "last_login", header: "i18n:govoplan-access.last_login.43dab84f", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_login_at || "", render: (row) => formatDateTime(row.last_login_at) }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 190, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, { id: "explain", label: i18nMessage("i18n:govoplan-access.explain_access_for_value.3af96e47", { value0: row.email }), icon: , onClick: () => void openAccessExplanation(row) }, { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: , disabled: !(canUpdate || canSuspend || canManageGroups || canAssignRoles), onClick: () => openEdit(row) }, { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: , variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.is_last_active_owner, onClick: () => setDeactivating(row) } ]} /> }], [canAssignRoles, canManageGroups, canSuspend, canUpdate, settings]); return ( <> } variant="primary" onClick={openCreate} disabled={!canCreate} />}>
row.id} emptyText="i18n:govoplan-access.no_tenant_users_found.74bb615f" />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, email: event.target.value })} /> setDraft({ ...draft, displayName: event.target.value })} /> {editing === "new" && setDraft({ ...draft, password })} /> }
{editing === "new" && setDraft({ ...draft, passwordResetRequired })} />} {editing && editing !== "new" && editing.is_last_active_owner &&

i18n:govoplan-access.this_membership_is_the_tenant_s_last_active_oper.072b247f

}
i18n:govoplan-access.groups.ae9629f4 group.is_active).map((group) => ({ id: group.id, label: group.name, description: group.description, disabled: !canManageGroups }))} selected={draft.groupIds} onChange={(groupIds) => setDraft({ ...draft, groupIds })} emptyText="i18n:govoplan-access.no_groups_exist_yet.9cd029f6" />
i18n:govoplan-access.direct_roles.c4db7156 ({ id: role.id, label: role.name, description: role.description, disabled: !canAssignRoles }))} selected={draft.roleIds} onChange={(roleIds) => setDraft({ ...draft, roleIds })} emptyText="i18n:govoplan-access.no_assignable_roles_exist.a4c268c2" />

i18n:govoplan-access.group_roles_and_direct_roles_are_combined_the_ba.a43c2f16

setViewing(null)} className="admin-dialog admin-dialog-wide" footer={}> {viewing &&
i18n:govoplan-access.user.9f8a2389
{viewing.display_name || viewing.email}
i18n:govoplan-access.email.84add5b2
{viewing.email}
i18n:govoplan-access.membership.53bc9670
{viewing.is_active ? "i18n:govoplan-access.active.a733b809" : "i18n:govoplan-access.inactive.09af574c"}
i18n:govoplan-access.global_account.e1b00cf5
{viewing.account_is_active ? "i18n:govoplan-access.active.a733b809" : "i18n:govoplan-access.inactive.09af574c"}
i18n:govoplan-access.groups.ae9629f4
{joinLabels(viewing.groups)}
i18n:govoplan-access.direct_roles.c4db7156
{joinLabels(viewing.roles)}
i18n:govoplan-access.effective_permissions.17c0fe8a
{hasTenantWildcard(viewing.effective_scopes) ? "i18n:govoplan-access.all_tenant_permissions.cca8b6e4" : viewing.effective_scopes.join(", ") || "i18n:govoplan-access.none.6eef6648"}
i18n:govoplan-access.last_login.43dab84f
{formatDateTime(viewing.last_login_at)}
}
{ if (!accessExplanationLoading) { setExplaining(null); setAccessExplanation(null); } }} className="admin-dialog admin-dialog-wide" footer={}> {accessExplanationLoading &&

i18n:govoplan-access.loading_access_explanation.04a7c934

} {accessExplanation && <>
i18n:govoplan-access.user.9f8a2389
{accessExplanation.user.display_name || accessExplanation.user.email}
i18n:govoplan-access.account.85dfa32c
{accessExplanation.user.account_id}
i18n:govoplan-access.role_sources.6f42a672
{accessExplanation.role_sources.length}
i18n:govoplan-access.function_facts.848b32cc
{accessExplanation.function_facts.length}

i18n:govoplan-access.role_sources.6f42a672

{accessExplanation.role_sources.length ?
{accessExplanation.role_sources.map((source) =>
{source.role_name}
{sourceTypeLabel(source)} {source.group_name && <> - i18n:govoplan-access.group.171a0606: {source.group_name}} {(source.function_name || source.function_id) && <> - i18n:govoplan-access.function_fact.4f7435e4: {source.function_name || source.function_id}} {(source.organization_unit_name || source.organization_unit_id) && <> - i18n:govoplan-access.organization_unit.9832b383: {source.organization_unit_name || source.organization_unit_id}}
{source.permissions.map((scope) => {scope})}
)}
:

i18n:govoplan-access.no_role_sources_found.91013aa5

}

i18n:govoplan-access.function_facts.848b32cc

{accessExplanation.function_facts.length ?
{accessExplanation.function_facts.map((fact) =>
{fact.function_name || fact.function_id}
i18n:govoplan-access.organization_unit.9832b383: {fact.organization_unit_name || fact.organization_unit_id}
i18n:govoplan-access.assignment_source.5fac1f72: {fact.assignment_source}
i18n:govoplan-access.mapped_roles.504d9ff9: {factRoles(fact)}
)}
:

i18n:govoplan-access.no_function_facts_found.b2ecee17

}

i18n:govoplan-access.effective_permissions.17c0fe8a

{accessExplanation.scopes.map((scope) => source.role_name).join(", ")}>{scope.scope})}
}
setTemporaryPassword(null)} className="admin-dialog" footer={}> {temporaryPassword && <>

i18n:govoplan-access.this_is_shown_once_for.b0f0f526 {temporaryPassword.email}.

{temporaryPassword.password}

i18n:govoplan-access.transmit_it_through_a_separate_secure_channel.ab892c7b

}
setDeactivating(null)} onConfirm={() => void deactivate()} /> ); } function draftKey(draft: typeof emptyDraft): string { return JSON.stringify(draft); } function sortUsers(left: UserAdminItem, right: UserAdminItem): number { return (left.display_name || left.email).localeCompare(right.display_name || right.email) || left.email.localeCompare(right.email); } function sortGroups(left: GroupSummary, right: GroupSummary): number { return left.name.localeCompare(right.name); } function sortTenantRoles(left: RoleSummary, right: RoleSummary): number { const builtinDelta = Number(right.is_builtin) - Number(left.is_builtin); if (builtinDelta !== 0) return builtinDelta; return left.name.localeCompare(right.name); } function sourceKey(source: AccessRoleSourceItem): string { return [ source.source_type, source.role_id, source.group_id || "", source.function_assignment_id || "", source.function_id || "" ].join(":"); } function AccessExplanationLinks({ organizationHref, idmHref }: { organizationHref: string | null; idmHref: string | null }) { if (!organizationHref && !idmHref) return null; return (
{organizationHref && i18n:govoplan-access.open_organizations} {organizationHref && idmHref && | } {idmHref && i18n:govoplan-access.open_idm_assignment}
); } function organizationHrefForSource(source: AccessRoleSourceItem): string | null { return organizationHref(source.function_id, source.organization_unit_id); } function organizationHrefForFact(fact: FunctionFactExplanationItem): string | null { return organizationHref(fact.function_id, fact.organization_unit_id); } function organizationHref(functionId?: string | null, unitId?: string | null): string | null { const params = new URLSearchParams(); if (functionId) { params.set("section", "functions"); params.set("function_id", functionId); if (unitId) params.set("unit_id", unitId); } else if (unitId) { params.set("section", "units"); params.set("unit_id", unitId); } const query = params.toString(); return query ? `/organizations?${query}` : null; } function idmHrefForSource(source: AccessRoleSourceItem): string | null { return idmHref(source.function_assignment_id, source.function_id); } function idmHrefForFact(fact: FunctionFactExplanationItem): string | null { return idmHref(fact.assignment_id, fact.function_id); } function idmHref(assignmentId?: string | null, functionId?: string | null): string | null { const params = new URLSearchParams(); if (assignmentId) params.set("assignment_id", assignmentId); if (functionId) params.set("function_id", functionId); const query = params.toString(); return query ? `/idm?${query}` : null; } function sourceTypeLabel(source: AccessRoleSourceItem): string { switch (source.source_type) { case "direct_role": return "i18n:govoplan-access.direct_role_source.bf3e6a27"; case "group_role": return "i18n:govoplan-access.group_role_source.0aa38904"; case "legacy_function_role": return "i18n:govoplan-access.legacy_function_role_source.36f81c6b"; case "idm_function_role": return "i18n:govoplan-access.idm_function_role_source.14a231fe"; case "system_role": return "i18n:govoplan-access.system_role_source.c6e83223"; default: return source.source_type; } } function factRoles(fact: FunctionFactExplanationItem): string { return fact.role_names.length ? fact.role_names.join(", ") : "i18n:govoplan-access.no_mapped_roles.986bfb44"; }