feat: add access module boundary migrations
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
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, fetchTenants, updateTenant, type SystemSettingsItem, type TenantAdminItem, type TenantOwnerCandidate } from "../../api/admin";
|
||||
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 } 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 = {
|
||||
@@ -54,35 +55,46 @@ export default function TenantsPanel({
|
||||
canUpdate,
|
||||
canSuspend,
|
||||
onAuthRefresh
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
canCreate: boolean;
|
||||
canUpdate: boolean;
|
||||
canSuspend: boolean;
|
||||
onAuthRefresh: () => Promise<void>;
|
||||
}) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;onAuthRefresh: () => Promise<void>;}) {
|
||||
const [tenants, setTenants] = useState<TenantAdminItem[]>([]);
|
||||
const [systemSettings, setSystemSettings] = useState<SystemSettingsItem | null>(null);
|
||||
const [ownerCandidates, setOwnerCandidates] = useState<TenantOwnerCandidate[]>([]);
|
||||
const tenantsRef = useRef<TenantAdminItem[]>([]);
|
||||
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
||||
const [editing, setEditing] = useState<TenantAdminItem | "new" | null>(null);
|
||||
const [viewing, setViewing] = useState<TenantAdminItem | null>(null);
|
||||
const [draft, setDraft] = useState<TenantDraft>(emptyDraft);
|
||||
const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft));
|
||||
const [confirmSuspend, setConfirmSuspend] = useState<TenantAdminItem | 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 [nextTenants, nextOwnerCandidates, nextSystemSettings] = await Promise.all([
|
||||
fetchTenants(settings),
|
||||
canCreate ? fetchTenantOwnerCandidates(settings) : Promise.resolve([]),
|
||||
fetchSystemSettings(settings).catch(() => null)
|
||||
]);
|
||||
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);
|
||||
@@ -93,16 +105,22 @@ export default function TenantsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
|
||||
useEffect(() => {
|
||||
tenantsRef.current = [];
|
||||
resetDeltaWatermark();
|
||||
void load();
|
||||
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
|
||||
|
||||
function openCreate() {
|
||||
setDraft({ ...emptyDraft, ownerAccountId: auth.user.account_id });
|
||||
const nextDraft = { ...emptyDraft, ownerAccountId: auth.user.account_id };
|
||||
setDraft(nextDraft);
|
||||
setSavedDraftKey(draftKey(nextDraft));
|
||||
setEditing("new");
|
||||
setError("");
|
||||
}
|
||||
|
||||
function openEdit(tenant: TenantAdminItem) {
|
||||
setDraft({
|
||||
const nextDraft = {
|
||||
slug: tenant.slug,
|
||||
name: tenant.name,
|
||||
ownerAccountId: "",
|
||||
@@ -112,12 +130,20 @@ export default function TenantsPanel({
|
||||
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("");
|
||||
}
|
||||
|
||||
async function save() {
|
||||
function closeEditor() {
|
||||
setEditing(null);
|
||||
setDraft(emptyDraft);
|
||||
setSavedDraftKey(draftKey(emptyDraft));
|
||||
}
|
||||
|
||||
async function save(): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -137,7 +163,7 @@ export default function TenantsPanel({
|
||||
...governance
|
||||
});
|
||||
const selectedOwner = ownerCandidates.find((candidate) => candidate.account_id === draft.ownerAccountId);
|
||||
setSuccess(`Tenant ${created.name} created with ${selectedOwner?.display_name || selectedOwner?.email || "the selected account"} as Owner.`);
|
||||
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<typeof updateTenant>[2] = {};
|
||||
@@ -151,13 +177,15 @@ export default function TenantsPanel({
|
||||
}
|
||||
if (canSuspend) payload.is_active = draft.isActive;
|
||||
await updateTenant(settings, editing.id, payload);
|
||||
setSuccess(`Tenant ${draft.name} updated.`);
|
||||
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);
|
||||
}
|
||||
@@ -169,7 +197,7 @@ export default function TenantsPanel({
|
||||
setError("");
|
||||
try {
|
||||
await updateTenant(settings, confirmSuspend.id, { is_active: false });
|
||||
setSuccess(`${confirmSuspend.name} suspended.`);
|
||||
setSuccess(i18nMessage("i18n:govoplan-access.value_suspended.31731a28", { value0: confirmSuspend.name }));
|
||||
setConfirmSuspend(null);
|
||||
await onAuthRefresh();
|
||||
await load();
|
||||
@@ -185,74 +213,82 @@ export default function TenantsPanel({
|
||||
const systemAllowsCustomRoles = systemSettings?.allow_tenant_custom_roles !== false;
|
||||
const systemAllowsApiKeys = systemSettings?.allow_tenant_api_keys !== false;
|
||||
const systemDeniedGovernance = [
|
||||
systemAllowsCustomGroups ? "" : "custom groups",
|
||||
systemAllowsCustomRoles ? "" : "custom roles",
|
||||
systemAllowsApiKeys ? "" : "API keys"
|
||||
].filter(Boolean).join(", ");
|
||||
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<DataGridColumn<TenantAdminItem>[]>(() => [
|
||||
{ id: "name", header: "Tenant", width: "minmax(210px, 1fr)", minWidth: 190, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div> },
|
||||
{ id: "users", header: "Users", 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: "Groups", width: 95, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.groups ?? 0 },
|
||||
{ id: "campaigns", header: "Campaigns", width: 110, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.campaigns ?? 0 },
|
||||
{ id: "files", header: "Files", width: 90, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.files ?? 0 },
|
||||
{ id: "locale", header: "Locale", width: 120, minWidth: 90, maxWidth: 220, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.default_locale },
|
||||
{ id: "status", header: "Status", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
|
||||
{ id: "actions", header: "Actions", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => <div className="admin-icon-actions">
|
||||
<AdminIconButton label={`Inspect ${row.name}`} icon={<Search />} onClick={() => setViewing(row)} />
|
||||
<AdminIconButton label={`Edit ${row.name}`} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canUpdate} />
|
||||
<AdminIconButton label={`Suspend ${row.name}`} icon={<Trash2 />} variant="danger" onClick={() => setConfirmSuspend(row)} disabled={!canSuspend || !row.is_active || row.id === activeTenantId} />
|
||||
</div> }
|
||||
], [activeTenantId, canSuspend, canUpdate]);
|
||||
{ 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) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div> },
|
||||
{ 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) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
|
||||
{ id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => <div className="admin-icon-actions">
|
||||
<AdminIconButton label={i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.name })} icon={<Search />} onClick={() => setViewing(row)} />
|
||||
<AdminIconButton label={i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name })} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canUpdate} />
|
||||
<AdminIconButton label={i18nMessage("i18n:govoplan-access.suspend_value.03a74b32", { value0: row.name })} icon={<Trash2 />} variant="danger" onClick={() => setConfirmSuspend(row)} disabled={!canSuspend || !row.is_active || row.id === activeTenantId} />
|
||||
</div> }],
|
||||
[activeTenantId, canSuspend, canUpdate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminPageLayout
|
||||
title="Tenants"
|
||||
description="Create and govern tenant spaces. Suspension retains campaigns, files and audit evidence; the tenant backing the current session cannot be suspended."
|
||||
title="i18n:govoplan-access.tenants.1f7ae776"
|
||||
description="i18n:govoplan-access.create_and_govern_tenant_spaces_suspension_retai.1b76d377"
|
||||
loading={loading}
|
||||
error={error}
|
||||
success={success}
|
||||
actions={<><Button onClick={() => void load()} disabled={loading}>Reload</Button><AdminIconButton label="Add tenant" icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canCreate} /></>}
|
||||
>
|
||||
<div className="admin-table-surface"><DataGrid id="admin-tenants-v3" rows={tenants} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText="No tenants found." /></div>
|
||||
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-access.reload.cce71553</Button><AdminIconButton label="i18n:govoplan-access.add_tenant.b8e32af0" icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canCreate} /></>}>
|
||||
|
||||
<div className="admin-table-surface"><DataGrid id="admin-tenants-v3" rows={tenants} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText="i18n:govoplan-access.no_tenants_found.72d04cf4" /></div>
|
||||
</AdminPageLayout>
|
||||
|
||||
<Dialog open={editing !== null} title={editing === "new" ? "Create tenant" : "Edit tenant"} onClose={() => !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<><Button onClick={() => setEditing(null)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void save()} disabled={!(editing === "new" ? canCreate : canUpdate) || busy || !draft.name.trim() || !draft.slug.trim() || (editing === "new" && !draft.ownerAccountId)}>{busy ? "Saving…" : "Save tenant"}</Button></>}>
|
||||
<Dialog open={editing !== null} title={editing === "new" ? "i18n:govoplan-access.create_tenant.4dbd55d9" : "i18n:govoplan-access.edit_tenant.e2ba43f9"} onClose={() => !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<><Button onClick={() => setEditing(null)} disabled={busy}>i18n:govoplan-access.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabled={!(editing === "new" ? canCreate : canUpdate) || busy || !draft.name.trim() || !draft.slug.trim() || editing === "new" && !draft.ownerAccountId}>{busy ? "i18n:govoplan-access.saving.56a2285c" : "i18n:govoplan-access.save_tenant.9eb2ac74"}</Button></>}>
|
||||
<div className="admin-form-grid two-columns">
|
||||
<FormField label="Name"><input value={draft.name} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||
<FormField label="Slug"><input value={draft.slug} disabled={editing !== "new" || !canCreate} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
|
||||
{editing === "new" && <FormField label="Initial tenant owner"><select value={draft.ownerAccountId} onChange={(event) => setDraft({ ...draft, ownerAccountId: event.target.value })}>{ownerCandidates.map((candidate) => <option key={candidate.account_id} value={candidate.account_id}>{candidate.display_name ? `${candidate.display_name} (${candidate.email})` : candidate.email}</option>)}</select></FormField>}
|
||||
<FormField label="Default locale"><input value={draft.defaultLocale} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, defaultLocale: event.target.value })} /></FormField>
|
||||
{editing !== "new" && <FormField label="Status"><select value={draft.isActive ? "active" : "inactive"} disabled={!canSuspend} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">Active</option><option value="inactive">Suspended</option></select></FormField>}
|
||||
<FormField label="Description"><textarea rows={4} value={draft.description} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||
<FormField label="i18n:govoplan-access.name.709a2322"><input value={draft.name} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||
<FormField label="i18n:govoplan-access.slug.094da9b9"><input value={draft.slug} disabled={editing !== "new" || !canCreate} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
|
||||
{editing === "new" && <FormField label="i18n:govoplan-access.initial_tenant_owner.682291a9"><select value={draft.ownerAccountId} onChange={(event) => setDraft({ ...draft, ownerAccountId: event.target.value })}>{ownerCandidates.map((candidate) => <option key={candidate.account_id} value={candidate.account_id}>{candidate.display_name ? i18nMessage("i18n:govoplan-access.value_value.c189e8bc", { value0: candidate.display_name, value1: candidate.email }) : candidate.email}</option>)}</select></FormField>}
|
||||
<FormField label="i18n:govoplan-access.default_locale.b99d021f"><input value={draft.defaultLocale} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, defaultLocale: event.target.value })} /></FormField>
|
||||
{editing !== "new" && <FormField label="i18n:govoplan-access.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} disabled={!canSuspend} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-access.active.a733b809</option><option value="inactive">i18n:govoplan-access.suspended.794696a7</option></select></FormField>}
|
||||
<FormField label="i18n:govoplan-access.description.55f8ebc8"><textarea rows={4} value={draft.description} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||
</div>
|
||||
<h3>System governance overrides</h3>
|
||||
<h3>i18n:govoplan-access.system_governance_overrides.97cdf3ce</h3>
|
||||
<div className="admin-form-grid two-columns">
|
||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsCustomGroups} label="Custom tenant groups" value={draft.customGroups} onChange={(customGroups) => setDraft({ ...draft, customGroups })} />
|
||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsCustomRoles} label="Custom tenant roles" value={draft.customRoles} onChange={(customRoles) => setDraft({ ...draft, customRoles })} />
|
||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsApiKeys} label="Tenant API keys" value={draft.apiKeys} onChange={(apiKeys) => setDraft({ ...draft, apiKeys })} />
|
||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsCustomGroups} label="i18n:govoplan-access.custom_tenant_groups.570ee603" value={draft.customGroups} onChange={(customGroups) => setDraft({ ...draft, customGroups })} />
|
||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsCustomRoles} label="i18n:govoplan-access.custom_tenant_roles.a738c37c" value={draft.customRoles} onChange={(customRoles) => setDraft({ ...draft, customRoles })} />
|
||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsApiKeys} label="i18n:govoplan-access.tenant_api_keys.4b1d81f8" value={draft.apiKeys} onChange={(apiKeys) => setDraft({ ...draft, apiKeys })} />
|
||||
</div>
|
||||
<p className="muted small-note">Inherit follows the current system setting. Explicit deny narrows access; explicit allow is valid only while the system setting allows it.</p>
|
||||
{systemDeniedGovernance && <p className="muted small-note">Explicit allow is unavailable for {systemDeniedGovernance} because the current system setting denies it.</p>}
|
||||
<p className="muted small-note">i18n:govoplan-access.inherit_follows_the_current_system_setting_expli.60d4d868</p>
|
||||
{systemDeniedGovernance && <p className="muted small-note">i18n:govoplan-access.explicit_allow_is_unavailable_for.8d05fd4a {systemDeniedGovernance} i18n:govoplan-access.because_the_current_system_setting_denies_it.3f59c244</p>}
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={Boolean(viewing)} title="Tenant details" onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>Close</Button>}>
|
||||
<Dialog open={Boolean(viewing)} title="i18n:govoplan-access.tenant_details.5976ba72" onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-access.close.bbfa773e</Button>}>
|
||||
{viewing && <><dl className="admin-details-grid">
|
||||
<div><dt>Tenant</dt><dd>{viewing.name}</dd></div><div><dt>Slug</dt><dd>{viewing.slug}</dd></div>
|
||||
<div><dt>Status</dt><dd>{viewing.is_active ? "Active" : "Suspended"}</dd></div><div><dt>Default locale</dt><dd>{viewing.default_locale}</dd></div>
|
||||
<div><dt>Created</dt><dd>{formatDateTime(viewing.created_at)}</dd></div><div><dt>Updated</dt><dd>{formatDateTime(viewing.updated_at)}</dd></div>
|
||||
<div><dt>Custom groups</dt><dd>{viewing.effective_governance.allow_custom_groups ? "Allowed" : "Denied"} ({fromOverride(viewing.allow_custom_groups)})</dd></div>
|
||||
<div><dt>Custom roles</dt><dd>{viewing.effective_governance.allow_custom_roles ? "Allowed" : "Denied"} ({fromOverride(viewing.allow_custom_roles)})</dd></div>
|
||||
<div><dt>API keys</dt><dd>{viewing.effective_governance.allow_api_keys ? "Allowed" : "Denied"} ({fromOverride(viewing.allow_api_keys)})</dd></div>
|
||||
<div><dt>Objects</dt><dd>{viewing.counts.users ?? 0} users, {viewing.counts.groups ?? 0} groups, {viewing.counts.campaigns ?? 0} campaigns, {viewing.counts.files ?? 0} files</dd></div>
|
||||
<div><dt>i18n:govoplan-access.tenant.3ca93c78</dt><dd>{viewing.name}</dd></div><div><dt>i18n:govoplan-access.slug.094da9b9</dt><dd>{viewing.slug}</dd></div>
|
||||
<div><dt>i18n:govoplan-access.status.bae7d5be</dt><dd>{viewing.is_active ? "i18n:govoplan-access.active.a733b809" : "i18n:govoplan-access.suspended.794696a7"}</dd></div><div><dt>i18n:govoplan-access.default_locale.b99d021f</dt><dd>{viewing.default_locale}</dd></div>
|
||||
<div><dt>i18n:govoplan-access.created.accf40c8</dt><dd>{formatDateTime(viewing.created_at)}</dd></div><div><dt>i18n:govoplan-access.updated.f2f8570d</dt><dd>{formatDateTime(viewing.updated_at)}</dd></div>
|
||||
<div><dt>i18n:govoplan-access.custom_groups.1f7b7c8f</dt><dd>{viewing.effective_governance.allow_custom_groups ? "i18n:govoplan-access.allowed.77c7b490" : "i18n:govoplan-access.denied.63b16bd4"} ({fromOverride(viewing.allow_custom_groups)})</dd></div>
|
||||
<div><dt>i18n:govoplan-access.custom_roles.e78ef63d</dt><dd>{viewing.effective_governance.allow_custom_roles ? "i18n:govoplan-access.allowed.77c7b490" : "i18n:govoplan-access.denied.63b16bd4"} ({fromOverride(viewing.allow_custom_roles)})</dd></div>
|
||||
<div><dt>i18n:govoplan-access.api_keys.94fcf3c2</dt><dd>{viewing.effective_governance.allow_api_keys ? "i18n:govoplan-access.allowed.77c7b490" : "i18n:govoplan-access.denied.63b16bd4"} ({fromOverride(viewing.allow_api_keys)})</dd></div>
|
||||
<div><dt>i18n:govoplan-access.objects.72a83add</dt><dd>{viewing.counts.users ?? 0} i18n:govoplan-access.users.81651889 {viewing.counts.groups ?? 0} i18n:govoplan-access.groups.07551586 {viewing.counts.campaigns ?? 0} i18n:govoplan-access.campaigns.2282ffeb {viewing.counts.files ?? 0} files</dd></div>
|
||||
</dl>{viewing.description && <p>{viewing.description}</p>}</>}
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog open={Boolean(confirmSuspend)} title="Suspend tenant" message={`Suspend ${confirmSuspend?.name}? Existing data remains retained, but its members cannot use the tenant.`} confirmLabel="Suspend tenant" tone="danger" busy={busy} onCancel={() => setConfirmSuspend(null)} onConfirm={() => void suspend()} />
|
||||
</>
|
||||
);
|
||||
<ConfirmDialog open={Boolean(confirmSuspend)} title="i18n:govoplan-access.suspend_tenant.151d283a" message={i18nMessage("i18n:govoplan-access.suspend_value_existing_data_remains_retained_but.19bccd78", { value0: confirmSuspend?.name })} confirmLabel="i18n:govoplan-access.suspend_tenant.151d283a" tone="danger" busy={busy} onCancel={() => setConfirmSuspend(null)} onConfirm={() => void suspend()} />
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
function GovernanceSelect({ label, value, onChange, disabled = false, allowDisabled = false }: { label: string; value: OverrideValue; onChange: (value: OverrideValue) => void; disabled?: boolean; allowDisabled?: boolean }) {
|
||||
return <FormField label={label}><select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as OverrideValue)}><option value="inherit">Inherit system setting</option><option value="allow" disabled={allowDisabled}>Allow when system allows</option><option value="deny">Explicitly deny</option></select></FormField>;
|
||||
function GovernanceSelect({ label, value, onChange, disabled = false, allowDisabled = false }: {label: string;value: OverrideValue;onChange: (value: OverrideValue) => void;disabled?: boolean;allowDisabled?: boolean;}) {
|
||||
return <FormField label={label}><select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as OverrideValue)}><option value="inherit">i18n:govoplan-access.inherit_system_setting.7f125156</option><option value="allow" disabled={allowDisabled}>i18n:govoplan-access.allow_when_system_allows.4c5178cb</option><option value="deny">i18n:govoplan-access.explicitly_deny.17ad945a</option></select></FormField>;
|
||||
}
|
||||
|
||||
function draftKey(draft: TenantDraft): string {
|
||||
return JSON.stringify(draft);
|
||||
}
|
||||
|
||||
function sortTenants(left: TenantAdminItem, right: TenantAdminItem): number {
|
||||
return left.name.localeCompare(right.name) || left.slug.localeCompare(right.slug);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user