From aeda457fb1a7a725a9ffa193a8426b91e773afe1 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 31 Jul 2026 04:21:34 +0200 Subject: [PATCH] refactor: move tenant administration to tenancy --- src/govoplan_access/backend/manifest.py | 2 - webui/src/api/admin.ts | 80 ----- webui/src/features/admin/AdminPage.tsx | 15 - .../features/admin/TenantSettingsPanel.tsx | 166 ---------- webui/src/features/admin/TenantsPanel.tsx | 294 ------------------ webui/src/module.ts | 2 - 6 files changed, 559 deletions(-) delete mode 100644 webui/src/features/admin/TenantSettingsPanel.tsx delete mode 100644 webui/src/features/admin/TenantsPanel.tsx diff --git a/src/govoplan_access/backend/manifest.py b/src/govoplan_access/backend/manifest.py index fd05aff..21291ac 100644 --- a/src/govoplan_access/backend/manifest.py +++ b/src/govoplan_access/backend/manifest.py @@ -706,7 +706,6 @@ manifest = ModuleManifest( routes=(FrontendRoute(path="/admin", component="AdminPage", required_any=ADMIN_READ_SCOPES, order=900),), nav_items=(NavItem(path="/admin", label="Admin", icon="admin", required_any=ADMIN_READ_SCOPES, order=900),), view_surfaces=( - ViewSurface(id="access.admin.system-tenants", module_id="access", kind="section", label="System tenants", order=10), ViewSurface(id="access.admin.system-roles", module_id="access", kind="section", label="System roles", order=20), ViewSurface(id="access.admin.system-users", module_id="access", kind="section", label="System users", order=50), ViewSurface(id="access.admin.system-credentials", module_id="access", kind="section", label="System credentials", order=80), @@ -716,7 +715,6 @@ manifest = ModuleManifest( ViewSurface(id="access.admin.tenant-users", module_id="access", kind="section", label="Tenant users", order=40), ViewSurface(id="access.admin.tenant-credentials", module_id="access", kind="section", label="Tenant credentials", order=70), ViewSurface(id="access.admin.tenant-api-keys", module_id="access", kind="section", label="Tenant API keys", order=80), - ViewSurface(id="access.admin.tenant-settings", module_id="access", kind="section", label="Tenant settings", order=90), ViewSurface(id="access.admin.group-credentials", module_id="access", kind="section", label="Group credentials", order=30), ViewSurface(id="access.admin.user-credentials", module_id="access", kind="section", label="User credentials", order=30), ViewSurface(id="access.settings.credentials", module_id="access", kind="section", label="Personal credentials", order=30), diff --git a/webui/src/api/admin.ts b/webui/src/api/admin.ts index 72254a6..7397f62 100644 --- a/webui/src/api/admin.ts +++ b/webui/src/api/admin.ts @@ -20,12 +20,6 @@ export type { TenantAdminItem } from "@govoplan/core-webui"; -export type TenantOwnerCandidate = { - account_id: string; - email: string; - display_name?: string | null; -}; - export type RoleSummary = { id: string; slug: string; @@ -178,24 +172,6 @@ export type LanguagePackage = { native_label?: string | null; }; -export type TenantSettingsItem = { - id: string; - slug: string; - name: string; - default_locale: string; - available_languages: LanguagePackage[]; - system_enabled_language_codes: string[]; - enabled_language_codes: string[]; - settings: Record; -}; - -export type TenantSettingsDeltaSections = Partial<{ - identity: Pick; - locale: Pick; - languages: Pick; - settings: Pick["settings"]; -}>; - export type GovernanceAssignment = { tenant_id: string; mode: "available" | "required"; @@ -251,14 +227,8 @@ export type GroupListDeltaResponse = { groups: GroupSummary[] } & DeltaResponseF export type RoleListDeltaResponse = { roles: RoleSummary[] } & DeltaResponseFields; export type SystemAccountListDeltaResponse = { accounts: SystemAccountItem[]; roles: RoleSummary[] } & DeltaResponseFields; export type ApiKeyListDeltaResponse = { api_keys: ApiKeyAdminItem[] } & DeltaResponseFields; -export type TenantListDeltaResponse = { tenants: TenantAdminItem[] } & DeltaResponseFields; export type GovernanceTemplateListDeltaResponse = { templates: GovernanceTemplateItem[] } & DeltaResponseFields; export type ExternalFunctionRoleMappingListDeltaResponse = { mappings: ExternalFunctionRoleMappingItem[] } & DeltaResponseFields; -export type TenantSettingsDeltaResponse = { - item?: TenantSettingsItem | null; - sections: TenantSettingsDeltaSections; - changed_sections: string[]; -} & DeltaResponseFields; function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string { return apiQuery(options); @@ -266,56 +236,6 @@ function deltaSuffix(options: { since?: string | null; limit?: number } = {}): s -export function fetchTenantsDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise { - const suffix = deltaSuffix(options); - return apiFetch(settings, `/api/v1/admin/tenants/delta${suffix}`); -} - -export async function fetchTenantOwnerCandidates(settings: ApiSettings): Promise { - return apiGetList(settings, "/api/v1/admin/tenants/owner-candidates", "accounts"); -} - -export function createTenant(settings: ApiSettings, payload: { - slug: string; - name: string; - owner_account_id?: string | null; - description?: string | null; - default_locale?: string; - settings?: Record; - allow_custom_groups?: boolean | null; - allow_custom_roles?: boolean | null; - allow_api_keys?: boolean | null; -}): Promise { - return apiFetch(settings, "/api/v1/admin/tenants", { method: "POST", body: JSON.stringify(payload) }); -} - -export function updateTenant(settings: ApiSettings, tenantId: string, payload: Partial<{ - name: string; - description: string | null; - default_locale: string; - settings: Record; - allow_custom_groups?: boolean | null; - allow_custom_roles?: boolean | null; - allow_api_keys?: boolean | null; - effective_governance: Record; - is_active: boolean; -}>): Promise { - return apiFetch(settings, `/api/v1/admin/tenants/${tenantId}`, { method: "PATCH", body: JSON.stringify(payload) }); -} - -export function fetchTenantSettings(settings: ApiSettings): Promise { - return apiFetch(settings, "/api/v1/admin/tenant/settings"); -} - -export function fetchTenantSettingsDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise { - const suffix = deltaSuffix(options); - return apiFetch(settings, `/api/v1/admin/tenant/settings/delta${suffix}`); -} - -export function updateTenantSettings(settings: ApiSettings, payload: { default_locale: string; enabled_language_codes?: string[] | null }): Promise { - return apiFetch(settings, "/api/v1/admin/tenant/settings", { method: "PATCH", body: JSON.stringify(payload) }); -} - export async function fetchUsers(settings: ApiSettings): Promise { const response = await apiFetch<{ users: UserAdminItem[] }>(settings, "/api/v1/admin/users"); return response.users; diff --git a/webui/src/features/admin/AdminPage.tsx b/webui/src/features/admin/AdminPage.tsx index 26a4c16..7f7f1eb 100644 --- a/webui/src/features/admin/AdminPage.tsx +++ b/webui/src/features/admin/AdminPage.tsx @@ -19,9 +19,7 @@ import { } from "@govoplan/core-webui"; import { adminReadScopes, hasAnyScope, hasScope } from "@govoplan/core-webui"; import SystemUsersPanel from "./SystemUsersPanel"; -import TenantSettingsPanel from "./TenantSettingsPanel"; import SystemRolesPanel from "./SystemRolesPanel"; -import TenantsPanel from "./TenantsPanel"; import UsersPanel from "./UsersPanel"; import GroupsPanel from "./GroupsPanel"; import RolesPanel from "./RolesPanel"; @@ -57,7 +55,6 @@ const handledAdminSectionIds = new Set([ "system-configuration-changes", "system-configuration-packages", "system-modules", - "system-tenants", "system-roles", "system-role-templates", "system-groups", @@ -65,7 +62,6 @@ const handledAdminSectionIds = new Set([ "system-file-connectors", "system-mail-servers", "system-credentials", - "tenant-settings", "tenant-roles", "tenant-function-role-mappings", "tenant-groups", @@ -83,7 +79,6 @@ const handledAdminSectionIds = new Set([ ]); const builtInAdminSurfaceIds: Record = { - "system-tenants": "access.admin.system-tenants", "system-roles": "access.admin.system-roles", "system-users": "access.admin.system-users", "system-credentials": "access.admin.system-credentials", @@ -93,7 +88,6 @@ const builtInAdminSurfaceIds: Record = { "tenant-users": "access.admin.tenant-users", "tenant-credentials": "access.admin.tenant-credentials", "tenant-api-keys": "access.admin.tenant-api-keys", - "tenant-settings": "access.admin.tenant-settings", "tenant-group-credentials": "access.admin.group-credentials", "tenant-user-credentials": "access.admin.user-credentials", "system-mail-servers": "mail.admin.system-servers", @@ -112,7 +106,6 @@ const builtInAdminSectionMetadata: Record< "system-file-connectors": { moduleId: "files", kind: "settings" }, "system-mail-servers": { moduleId: "mail", kind: "settings" }, "system-credentials": { moduleId: "access", kind: "settings" }, - "tenant-settings": { moduleId: "access", kind: "settings" }, "tenant-file-connectors": { moduleId: "files", kind: "settings" }, "tenant-mail-servers": { moduleId: "mail", kind: "settings" }, "tenant-credentials": { moduleId: "access", kind: "settings" }, @@ -174,7 +167,6 @@ export default function AdminPage({ if (hasAnyScope(auth, ["system:settings:read", "access:system_credential:read"])) { sections.add("system-credentials"); } - if (hasScope(auth, "system:tenants:read")) sections.add("system-tenants"); if (hasAnyScope(auth, ["system:accounts:read", "system:access:read"])) sections.add("system-users"); if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles"); if (hasScope(auth, "admin:users:read")) sections.add("tenant-users"); @@ -191,7 +183,6 @@ export default function AdminPage({ if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-file-connectors"); if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-file-connectors"); } - if (hasScope(auth, "admin:settings:read")) sections.add("tenant-settings"); if (hasAnyScope(auth, ["admin:settings:read", "access:credential:read"])) { sections.add("tenant-credentials"); if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-credentials"); @@ -254,7 +245,6 @@ export default function AdminPage({ { title: "GLOBAL", items: sortNavItems([ - visibleNavItem(available, "system-tenants", "i18n:govoplan-access.tenants.1f7ae776", 10), visibleNavItem(available, "system-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20), visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30), visibleNavItem(available, "system-groups", "i18n:govoplan-access.group_templates", 40), @@ -277,7 +267,6 @@ export default function AdminPage({ visibleNavItem(available, "tenant-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 60), visibleNavItem(available, "tenant-credentials", "i18n:govoplan-core.credentials.dd097a22", 70), visibleNavItem(available, "tenant-api-keys", "i18n:govoplan-access.api_keys.94fcf3c2", 80), - visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90), ...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds) ]) }, @@ -325,9 +314,6 @@ export default function AdminPage({ canWrite={hasAnyScope(auth, ["system:settings:write", "access:system_credential:write"])} /> )} - {!contributedSection && active === "system-tenants" && ( - - )} {!contributedSection && active === "system-users" && ( } {!contributedSection && active === "tenant-user-file-connectors" && } {!contributedSection && active === "tenant-group-file-connectors" && } - {!contributedSection && active === "tenant-settings" && } diff --git a/webui/src/features/admin/TenantSettingsPanel.tsx b/webui/src/features/admin/TenantSettingsPanel.tsx deleted file mode 100644 index 430edef..0000000 --- a/webui/src/features/admin/TenantSettingsPanel.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useEffect, useState } from "react"; -import type { ApiSettings } from "@govoplan/core-webui"; -import { Button } from "@govoplan/core-webui"; -import { Card } from "@govoplan/core-webui"; -import { FormField } from "@govoplan/core-webui"; -import { fetchTenantSettingsDelta, updateTenantSettings, type TenantSettingsDeltaSections, type TenantSettingsItem } from "../../api/admin"; -import { AdminPageLayout, AdminSelectionList, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; - -const DELTA_KEY = "access:tenant-settings"; - -const fallback: TenantSettingsItem = { - id: "", - slug: "", - name: "", - default_locale: "en", - available_languages: [ - { code: "en", label: "English", native_label: "English" }, - { code: "de", label: "German", native_label: "Deutsch" } - ], - system_enabled_language_codes: ["en", "de"], - enabled_language_codes: ["en", "de"], - settings: {} -}; - -export default function TenantSettingsPanel({ - settings, - canWrite, - onAuthRefresh - - - - -}: {settings: ApiSettings;canWrite: boolean;onAuthRefresh: () => Promise;}) { - const [draft, setDraft] = useState(fallback); - const [savedDraft, setSavedDraft] = useState(fallback); - const [loading, setLoading] = useState(true); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(""); - const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); - const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes); - const dirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft); - - useUnsavedDraftGuard({ - dirty, - onSave: save, - onDiscard: () => setDraft(savedDraft) - }); - - async function load() { - setLoading(true); - setError(""); - setSuccess(""); - try { - const wasDirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft); - const loaded = await fetchTenantSettingsDelta(settings, { since: getDeltaWatermark(DELTA_KEY) }); - setDeltaWatermark(DELTA_KEY, loaded.watermark); - if (loaded.full && loaded.item) { - setSavedDraft(loaded.item); - if (!wasDirty) setDraft(loaded.item); - } else if (loaded.changed_sections.length) { - setSavedDraft((current) => applyTenantSettingsSections(current, loaded.sections)); - if (!wasDirty) setDraft((current) => applyTenantSettingsSections(current, loaded.sections)); - } - } catch (err) { - setError(adminErrorMessage(err)); - } finally { - setLoading(false); - } - } - - useEffect(() => { - resetDeltaWatermark(DELTA_KEY); - void load(); - }, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]); - - async function save(): Promise { - setBusy(true); - setError(""); - setSuccess(""); - try { - const saved = await updateTenantSettings(settings, { default_locale: draft.default_locale, enabled_language_codes: draft.enabled_language_codes }); - setDraft(saved); - setSavedDraft(saved); - resetDeltaWatermark(DELTA_KEY); - setSuccess("i18n:govoplan-access.tenant_general_settings_saved.485e7681"); - await onAuthRefresh(); - return true; - } catch (err) { - setError(adminErrorMessage(err)); - return false; - } finally { - setBusy(false); - } - } - - function setEnabledLanguages(selected: string[]) { - const enabled = new Set(selected); - const nextEnabled = draft.system_enabled_language_codes.filter((item) => enabled.has(item)); - const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale); - setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale }); - } - - return ( - }> - -
- - - - - { - const language = draft.available_languages.find((item) => item.code === code); - return { id: code, label: code.toUpperCase(), description: languageOptionLabel(language ?? { code, label: code.toUpperCase() }), disabled: !canWrite || busy || code === draft.default_locale }; - })} - selected={draft.enabled_language_codes} - onChange={setEnabledLanguages} - /> -

i18n:govoplan-access.tenant_languages_help

-
-
i18n:govoplan-access.tenant.3ca93c78
{draft.name || "-"}
-
i18n:govoplan-access.slug.094da9b9
{draft.slug || "-"}
-
i18n:govoplan-access.available.7c62a142
{draft.available_languages.map((item) => item.code.toUpperCase()).join(", ") || "-"}
-
-
-
-
); - -} - -function languageOptionLabel(language: {code: string;label: string;native_label?: string | null}): string { - return `${language.code.toUpperCase()} - ${language.native_label || language.label}`; -} - -function localeOptions(current: string, enabled: string[]): string[] { - return [...new Set([current, ...enabled].filter((item) => item && item.trim()))]; -} - -function tenantSettingsDraftKey(item: TenantSettingsItem): string { - return JSON.stringify({ - default_locale: item.default_locale, - enabled_language_codes: item.enabled_language_codes - }); -} - -function applyTenantSettingsSections(item: TenantSettingsItem, sections: TenantSettingsDeltaSections): TenantSettingsItem { - return { - ...item, - ...(sections.identity ?? {}), - ...(sections.locale ?? {}), - ...(sections.languages ?? {}), - ...(sections.settings ? { settings: sections.settings } : {}) - }; -} diff --git a/webui/src/features/admin/TenantsPanel.tsx b/webui/src/features/admin/TenantsPanel.tsx deleted file mode 100644 index 55f068c..0000000 --- a/webui/src/features/admin/TenantsPanel.tsx +++ /dev/null @@ -1,294 +0,0 @@ -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, TableActionGroup, 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) }, - { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , disabled: !canUpdate, onClick: () => openEdit(row) }, - { id: "suspend", label: i18nMessage("i18n:govoplan-access.suspend_value.03a74b32", { value0: row.name }), icon: , variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.id === activeTenantId, onClick: () => setConfirmSuspend(row) } - ]} /> }], - [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" && } -