import { useEffect, useMemo, useRef, useState } from "react"; import { Pencil, Plus, Search, Trash2 } from "lucide-react"; import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { createTenant, fetchSystemSettings, fetchTenantOwnerCandidates, fetchTenantsDelta, updateTenant, type SystemSettingsItem, type TenantAdminItem, type TenantOwnerCandidate } 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 { StatusBadge } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { AdminIconButton, AdminPageLayout, adminErrorMessage, formatAdminDateTime as formatDateTime, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; type OverrideValue = "inherit" | "allow" | "deny"; type TenantDraft = { slug: string; name: string; ownerAccountId: string; description: string; defaultLocale: string; isActive: boolean; customGroups: OverrideValue; customRoles: OverrideValue; apiKeys: OverrideValue; }; const emptyDraft: TenantDraft = { slug: "", name: "", ownerAccountId: "", description: "", defaultLocale: "en", isActive: true, customGroups: "inherit", customRoles: "inherit", apiKeys: "inherit" }; function fromOverride(value?: boolean | null): OverrideValue { if (value === true) return "allow"; if (value === false) return "deny"; return "inherit"; } function toOverride(value: OverrideValue): boolean | null { if (value === "allow") return true; if (value === "deny") return false; return null; } export default function TenantsPanel({ settings, auth, canCreate, canUpdate, canSuspend, onAuthRefresh }: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;onAuthRefresh: () => Promise;}) { const [tenants, setTenants] = useState([]); const [systemSettings, setSystemSettings] = useState(null); const [ownerCandidates, setOwnerCandidates] = useState([]); const tenantsRef = useRef([]); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [draft, setDraft] = useState(emptyDraft); const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft)); const [confirmSuspend, setConfirmSuspend] = useState(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 [nextTenants, nextOwnerCandidates, nextSystemSettings] = await Promise.all([ loadDeltaRows(tenantsRef.current, "tenancy:tenants", getDeltaWatermark, setDeltaWatermark, (since) => fetchTenantsDelta(settings, { since }), (response) => response.tenants, (tenant) => tenant.id, "tenant", sortTenants), canCreate ? fetchTenantOwnerCandidates(settings) : Promise.resolve([]), fetchSystemSettings(settings).catch(() => null)] ); tenantsRef.current = nextTenants; setTenants(nextTenants); setOwnerCandidates(nextOwnerCandidates); setSystemSettings(nextSystemSettings); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } useEffect(() => { tenantsRef.current = []; resetDeltaWatermark(); void load(); }, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]); function openCreate() { const nextDraft = { ...emptyDraft, ownerAccountId: auth.user.account_id }; setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setEditing("new"); setError(""); } function openEdit(tenant: TenantAdminItem) { const nextDraft = { slug: tenant.slug, name: tenant.name, ownerAccountId: "", description: tenant.description || "", defaultLocale: tenant.default_locale || "en", isActive: tenant.is_active, customGroups: fromOverride(tenant.allow_custom_groups), customRoles: fromOverride(tenant.allow_custom_roles), apiKeys: fromOverride(tenant.allow_api_keys) }; setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setEditing(tenant); setError(""); } function closeEditor() { setEditing(null); setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); } async function save(): Promise { setBusy(true); setError(""); try { const governance = { allow_custom_groups: toOverride(draft.customGroups), allow_custom_roles: toOverride(draft.customRoles), allow_api_keys: toOverride(draft.apiKeys) }; if (editing === "new") { const created = await createTenant(settings, { slug: draft.slug, name: draft.name, owner_account_id: draft.ownerAccountId || null, description: draft.description || null, default_locale: draft.defaultLocale, settings: {}, ...governance }); const selectedOwner = ownerCandidates.find((candidate) => candidate.account_id === draft.ownerAccountId); setSuccess(i18nMessage("i18n:govoplan-access.tenant_value_created_with_value_as_owner.1c18b6fb", { value0: created.name, value1: selectedOwner?.display_name || selectedOwner?.email || "i18n:govoplan-access.the_selected_account.1211bfb9" })); await onAuthRefresh(); } else if (editing) { const payload: Parameters[2] = {}; if (canUpdate) { payload.name = draft.name; payload.description = draft.description || null; payload.default_locale = draft.defaultLocale; payload.allow_custom_groups = governance.allow_custom_groups; payload.allow_custom_roles = governance.allow_custom_roles; payload.allow_api_keys = governance.allow_api_keys; } if (canSuspend) payload.is_active = draft.isActive; await updateTenant(settings, editing.id, payload); setSuccess(i18nMessage("i18n:govoplan-access.tenant_value_updated.25b2c855", { value0: draft.name })); await onAuthRefresh(); } setEditing(null); await load(); return true; } catch (err) { setError(adminErrorMessage(err)); return false; } finally { setBusy(false); } } async function suspend() { if (!confirmSuspend) return; setBusy(true); setError(""); try { await updateTenant(settings, confirmSuspend.id, { is_active: false }); setSuccess(i18nMessage("i18n:govoplan-access.value_suspended.31731a28", { value0: confirmSuspend.name })); setConfirmSuspend(null); await onAuthRefresh(); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(false); } } const activeTenantId = (auth.active_tenant ?? auth.tenant).id; const systemAllowsCustomGroups = systemSettings?.allow_tenant_custom_groups !== false; const systemAllowsCustomRoles = systemSettings?.allow_tenant_custom_roles !== false; const systemAllowsApiKeys = systemSettings?.allow_tenant_api_keys !== false; const systemDeniedGovernance = [ systemAllowsCustomGroups ? "" : "i18n:govoplan-access.custom_groups.453a605c", systemAllowsCustomRoles ? "" : "i18n:govoplan-access.custom_roles.d48dc976", systemAllowsApiKeys ? "" : "i18n:govoplan-access.api_keys.94fcf3c2"]. filter(Boolean).join(", "); const columns = useMemo[]>(() => [ { id: "name", header: "i18n:govoplan-access.tenant.3ca93c78", width: "minmax(210px, 1fr)", minWidth: 190, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) =>
{row.name}
{row.slug}
}, { id: "users", header: "i18n:govoplan-access.users.57f2b181", width: 100, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.users ?? 0, render: (row) => `${row.counts.active_users ?? 0}/${row.counts.users ?? 0}` }, { id: "groups", header: "i18n:govoplan-access.groups.ae9629f4", width: 95, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.groups ?? 0 }, { id: "campaigns", header: "i18n:govoplan-access.campaigns.01a23a28", width: 110, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.campaigns ?? 0 }, { id: "files", header: "i18n:govoplan-access.files.6ce6c512", width: 90, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.files ?? 0 }, { id: "locale", header: "i18n:govoplan-access.locale.8970f0e6", width: 120, minWidth: 90, maxWidth: 220, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.default_locale }, { id: "status", header: "i18n:govoplan-access.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setViewing(row)} /> } onClick={() => openEdit(row)} disabled={!canUpdate} /> } variant="danger" onClick={() => setConfirmSuspend(row)} disabled={!canSuspend || !row.is_active || row.id === activeTenantId} />
}], [activeTenantId, canSuspend, canUpdate]); return ( <> } variant="primary" onClick={openCreate} disabled={!canCreate} />}>
row.id} emptyText="i18n:govoplan-access.no_tenants_found.72d04cf4" />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} /> {editing === "new" && } setDraft({ ...draft, defaultLocale: event.target.value })} /> {editing !== "new" && }