refactor: move tenant administration to tenancy
This commit is contained in:
@@ -706,7 +706,6 @@ manifest = ModuleManifest(
|
|||||||
routes=(FrontendRoute(path="/admin", component="AdminPage", required_any=ADMIN_READ_SCOPES, order=900),),
|
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),),
|
nav_items=(NavItem(path="/admin", label="Admin", icon="admin", required_any=ADMIN_READ_SCOPES, order=900),),
|
||||||
view_surfaces=(
|
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-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-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),
|
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-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-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-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.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.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),
|
ViewSurface(id="access.settings.credentials", module_id="access", kind="section", label="Personal credentials", order=30),
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ export type {
|
|||||||
TenantAdminItem
|
TenantAdminItem
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
|
|
||||||
export type TenantOwnerCandidate = {
|
|
||||||
account_id: string;
|
|
||||||
email: string;
|
|
||||||
display_name?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RoleSummary = {
|
export type RoleSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -178,24 +172,6 @@ export type LanguagePackage = {
|
|||||||
native_label?: string | null;
|
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<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TenantSettingsDeltaSections = Partial<{
|
|
||||||
identity: Pick<TenantSettingsItem, "id" | "slug" | "name">;
|
|
||||||
locale: Pick<TenantSettingsItem, "default_locale">;
|
|
||||||
languages: Pick<TenantSettingsItem, "available_languages" | "system_enabled_language_codes" | "enabled_language_codes">;
|
|
||||||
settings: Pick<TenantSettingsItem, "settings">["settings"];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GovernanceAssignment = {
|
export type GovernanceAssignment = {
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
mode: "available" | "required";
|
mode: "available" | "required";
|
||||||
@@ -251,14 +227,8 @@ export type GroupListDeltaResponse = { groups: GroupSummary[] } & DeltaResponseF
|
|||||||
export type RoleListDeltaResponse = { roles: RoleSummary[] } & DeltaResponseFields;
|
export type RoleListDeltaResponse = { roles: RoleSummary[] } & DeltaResponseFields;
|
||||||
export type SystemAccountListDeltaResponse = { accounts: SystemAccountItem[]; roles: RoleSummary[] } & DeltaResponseFields;
|
export type SystemAccountListDeltaResponse = { accounts: SystemAccountItem[]; roles: RoleSummary[] } & DeltaResponseFields;
|
||||||
export type ApiKeyListDeltaResponse = { api_keys: ApiKeyAdminItem[] } & DeltaResponseFields;
|
export type ApiKeyListDeltaResponse = { api_keys: ApiKeyAdminItem[] } & DeltaResponseFields;
|
||||||
export type TenantListDeltaResponse = { tenants: TenantAdminItem[] } & DeltaResponseFields;
|
|
||||||
export type GovernanceTemplateListDeltaResponse = { templates: GovernanceTemplateItem[] } & DeltaResponseFields;
|
export type GovernanceTemplateListDeltaResponse = { templates: GovernanceTemplateItem[] } & DeltaResponseFields;
|
||||||
export type ExternalFunctionRoleMappingListDeltaResponse = { mappings: ExternalFunctionRoleMappingItem[] } & 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 {
|
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
|
||||||
return apiQuery(options);
|
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<TenantListDeltaResponse> {
|
|
||||||
const suffix = deltaSuffix(options);
|
|
||||||
return apiFetch(settings, `/api/v1/admin/tenants/delta${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchTenantOwnerCandidates(settings: ApiSettings): Promise<TenantOwnerCandidate[]> {
|
|
||||||
return apiGetList<TenantOwnerCandidate, "accounts">(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<string, unknown>;
|
|
||||||
allow_custom_groups?: boolean | null;
|
|
||||||
allow_custom_roles?: boolean | null;
|
|
||||||
allow_api_keys?: boolean | null;
|
|
||||||
}): Promise<TenantAdminItem> {
|
|
||||||
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<string, unknown>;
|
|
||||||
allow_custom_groups?: boolean | null;
|
|
||||||
allow_custom_roles?: boolean | null;
|
|
||||||
allow_api_keys?: boolean | null;
|
|
||||||
effective_governance: Record<string, boolean>;
|
|
||||||
is_active: boolean;
|
|
||||||
}>): Promise<TenantAdminItem> {
|
|
||||||
return apiFetch(settings, `/api/v1/admin/tenants/${tenantId}`, { method: "PATCH", body: JSON.stringify(payload) });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchTenantSettings(settings: ApiSettings): Promise<TenantSettingsItem> {
|
|
||||||
return apiFetch(settings, "/api/v1/admin/tenant/settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchTenantSettingsDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<TenantSettingsDeltaResponse> {
|
|
||||||
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<TenantSettingsItem> {
|
|
||||||
return apiFetch(settings, "/api/v1/admin/tenant/settings", { method: "PATCH", body: JSON.stringify(payload) });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchUsers(settings: ApiSettings): Promise<UserAdminItem[]> {
|
export async function fetchUsers(settings: ApiSettings): Promise<UserAdminItem[]> {
|
||||||
const response = await apiFetch<{ users: UserAdminItem[] }>(settings, "/api/v1/admin/users");
|
const response = await apiFetch<{ users: UserAdminItem[] }>(settings, "/api/v1/admin/users");
|
||||||
return response.users;
|
return response.users;
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ import {
|
|||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { adminReadScopes, hasAnyScope, hasScope } from "@govoplan/core-webui";
|
import { adminReadScopes, hasAnyScope, hasScope } from "@govoplan/core-webui";
|
||||||
import SystemUsersPanel from "./SystemUsersPanel";
|
import SystemUsersPanel from "./SystemUsersPanel";
|
||||||
import TenantSettingsPanel from "./TenantSettingsPanel";
|
|
||||||
import SystemRolesPanel from "./SystemRolesPanel";
|
import SystemRolesPanel from "./SystemRolesPanel";
|
||||||
import TenantsPanel from "./TenantsPanel";
|
|
||||||
import UsersPanel from "./UsersPanel";
|
import UsersPanel from "./UsersPanel";
|
||||||
import GroupsPanel from "./GroupsPanel";
|
import GroupsPanel from "./GroupsPanel";
|
||||||
import RolesPanel from "./RolesPanel";
|
import RolesPanel from "./RolesPanel";
|
||||||
@@ -57,7 +55,6 @@ const handledAdminSectionIds = new Set<string>([
|
|||||||
"system-configuration-changes",
|
"system-configuration-changes",
|
||||||
"system-configuration-packages",
|
"system-configuration-packages",
|
||||||
"system-modules",
|
"system-modules",
|
||||||
"system-tenants",
|
|
||||||
"system-roles",
|
"system-roles",
|
||||||
"system-role-templates",
|
"system-role-templates",
|
||||||
"system-groups",
|
"system-groups",
|
||||||
@@ -65,7 +62,6 @@ const handledAdminSectionIds = new Set<string>([
|
|||||||
"system-file-connectors",
|
"system-file-connectors",
|
||||||
"system-mail-servers",
|
"system-mail-servers",
|
||||||
"system-credentials",
|
"system-credentials",
|
||||||
"tenant-settings",
|
|
||||||
"tenant-roles",
|
"tenant-roles",
|
||||||
"tenant-function-role-mappings",
|
"tenant-function-role-mappings",
|
||||||
"tenant-groups",
|
"tenant-groups",
|
||||||
@@ -83,7 +79,6 @@ const handledAdminSectionIds = new Set<string>([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const builtInAdminSurfaceIds: Record<string, string> = {
|
const builtInAdminSurfaceIds: Record<string, string> = {
|
||||||
"system-tenants": "access.admin.system-tenants",
|
|
||||||
"system-roles": "access.admin.system-roles",
|
"system-roles": "access.admin.system-roles",
|
||||||
"system-users": "access.admin.system-users",
|
"system-users": "access.admin.system-users",
|
||||||
"system-credentials": "access.admin.system-credentials",
|
"system-credentials": "access.admin.system-credentials",
|
||||||
@@ -93,7 +88,6 @@ const builtInAdminSurfaceIds: Record<string, string> = {
|
|||||||
"tenant-users": "access.admin.tenant-users",
|
"tenant-users": "access.admin.tenant-users",
|
||||||
"tenant-credentials": "access.admin.tenant-credentials",
|
"tenant-credentials": "access.admin.tenant-credentials",
|
||||||
"tenant-api-keys": "access.admin.tenant-api-keys",
|
"tenant-api-keys": "access.admin.tenant-api-keys",
|
||||||
"tenant-settings": "access.admin.tenant-settings",
|
|
||||||
"tenant-group-credentials": "access.admin.group-credentials",
|
"tenant-group-credentials": "access.admin.group-credentials",
|
||||||
"tenant-user-credentials": "access.admin.user-credentials",
|
"tenant-user-credentials": "access.admin.user-credentials",
|
||||||
"system-mail-servers": "mail.admin.system-servers",
|
"system-mail-servers": "mail.admin.system-servers",
|
||||||
@@ -112,7 +106,6 @@ const builtInAdminSectionMetadata: Record<
|
|||||||
"system-file-connectors": { moduleId: "files", kind: "settings" },
|
"system-file-connectors": { moduleId: "files", kind: "settings" },
|
||||||
"system-mail-servers": { moduleId: "mail", kind: "settings" },
|
"system-mail-servers": { moduleId: "mail", kind: "settings" },
|
||||||
"system-credentials": { moduleId: "access", kind: "settings" },
|
"system-credentials": { moduleId: "access", kind: "settings" },
|
||||||
"tenant-settings": { moduleId: "access", kind: "settings" },
|
|
||||||
"tenant-file-connectors": { moduleId: "files", kind: "settings" },
|
"tenant-file-connectors": { moduleId: "files", kind: "settings" },
|
||||||
"tenant-mail-servers": { moduleId: "mail", kind: "settings" },
|
"tenant-mail-servers": { moduleId: "mail", kind: "settings" },
|
||||||
"tenant-credentials": { moduleId: "access", 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"])) {
|
if (hasAnyScope(auth, ["system:settings:read", "access:system_credential:read"])) {
|
||||||
sections.add("system-credentials");
|
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:accounts:read", "system:access:read"])) sections.add("system-users");
|
||||||
if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles");
|
if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles");
|
||||||
if (hasScope(auth, "admin:users:read")) sections.add("tenant-users");
|
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:users:read")) sections.add("tenant-user-file-connectors");
|
||||||
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-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"])) {
|
if (hasAnyScope(auth, ["admin:settings:read", "access:credential:read"])) {
|
||||||
sections.add("tenant-credentials");
|
sections.add("tenant-credentials");
|
||||||
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-credentials");
|
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-credentials");
|
||||||
@@ -254,7 +245,6 @@ export default function AdminPage({
|
|||||||
{
|
{
|
||||||
title: "GLOBAL",
|
title: "GLOBAL",
|
||||||
items: sortNavItems([
|
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-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20),
|
||||||
visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30),
|
visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30),
|
||||||
visibleNavItem(available, "system-groups", "i18n:govoplan-access.group_templates", 40),
|
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-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 60),
|
||||||
visibleNavItem(available, "tenant-credentials", "i18n:govoplan-core.credentials.dd097a22", 70),
|
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-api-keys", "i18n:govoplan-access.api_keys.94fcf3c2", 80),
|
||||||
visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90),
|
|
||||||
...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds)
|
...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds)
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
@@ -325,9 +314,6 @@ export default function AdminPage({
|
|||||||
canWrite={hasAnyScope(auth, ["system:settings:write", "access:system_credential:write"])}
|
canWrite={hasAnyScope(auth, ["system:settings:write", "access:system_credential:write"])}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!contributedSection && active === "system-tenants" && (
|
|
||||||
<TenantsPanel settings={settings} auth={auth} canCreate={hasScope(auth, "system:tenants:create")} canUpdate={hasScope(auth, "system:tenants:update")} canSuspend={hasScope(auth, "system:tenants:suspend")} onAuthRefresh={refreshAuth} />
|
|
||||||
)}
|
|
||||||
{!contributedSection && active === "system-users" && (
|
{!contributedSection && active === "system-users" && (
|
||||||
<SystemUsersPanel
|
<SystemUsersPanel
|
||||||
settings={settings}
|
settings={settings}
|
||||||
@@ -353,7 +339,6 @@ export default function AdminPage({
|
|||||||
{!contributedSection && active === "tenant-group-credentials" && <CredentialEnvelopesPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["admin:settings:write", "access:credential:write"])} />}
|
{!contributedSection && active === "tenant-group-credentials" && <CredentialEnvelopesPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["admin:settings:write", "access:credential:write"])} />}
|
||||||
{!contributedSection && active === "tenant-user-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="user" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
{!contributedSection && active === "tenant-user-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="user" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
||||||
{!contributedSection && active === "tenant-group-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
{!contributedSection && active === "tenant-group-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
||||||
{!contributedSection && active === "tenant-settings" && <TenantSettingsPanel settings={settings} canWrite={hasScope(auth, "admin:settings:write")} onAuthRefresh={refreshAuth} />}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<void>;}) {
|
|
||||||
const [draft, setDraft] = useState<TenantSettingsItem>(fallback);
|
|
||||||
const [savedDraft, setSavedDraft] = useState<TenantSettingsItem>(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<boolean> {
|
|
||||||
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 (
|
|
||||||
<AdminPageLayout
|
|
||||||
title="i18n:govoplan-access.tenant_general_settings.db1c3ba8"
|
|
||||||
description="i18n:govoplan-access.settings_for_the_active_tenant_context.ad267b86"
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
success={success}
|
|
||||||
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-access.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy || !draft.default_locale.trim()}>{busy ? "i18n:govoplan-access.saving.ae7e8875" : "i18n:govoplan-access.save_general_settings.5c90f8c4"}</Button></>}>
|
|
||||||
|
|
||||||
<div className="admin-settings-form">
|
|
||||||
<Card title="i18n:govoplan-access.locale.8970f0e6">
|
|
||||||
<FormField label="i18n:govoplan-access.tenant_locale.8fc19914" help="i18n:govoplan-access.used_as_this_tenant_s_locale_default_for_tenant_.cf298b8b">
|
|
||||||
<select value={draft.default_locale} disabled={!canWrite || busy || defaultLocaleOptions.length === 0} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })}>
|
|
||||||
{defaultLocaleOptions.map((code) => {
|
|
||||||
const language = draft.available_languages.find((item) => item.code === code);
|
|
||||||
return <option key={code} value={code}>{languageOptionLabel(language ?? { code, label: code.toUpperCase() })}</option>;
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
</FormField>
|
|
||||||
<AdminSelectionList
|
|
||||||
options={draft.system_enabled_language_codes.map((code) => {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<p className="muted small-note">i18n:govoplan-access.tenant_languages_help</p>
|
|
||||||
<dl className="detail-list">
|
|
||||||
<div><dt>i18n:govoplan-access.tenant.3ca93c78</dt><dd>{draft.name || "-"}</dd></div>
|
|
||||||
<div><dt>i18n:govoplan-access.slug.094da9b9</dt><dd>{draft.slug || "-"}</dd></div>
|
|
||||||
<div><dt>i18n:govoplan-access.available.7c62a142</dt><dd>{draft.available_languages.map((item) => item.code.toUpperCase()).join(", ") || "-"}</dd></div>
|
|
||||||
</dl>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</AdminPageLayout>);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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 } : {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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<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([
|
|
||||||
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<boolean> {
|
|
||||||
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<typeof updateTenant>[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<DataGridColumn<TenantAdminItem>[]>(() => [
|
|
||||||
{ 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) => <TableActionGroup actions={[
|
|
||||||
{ id: "inspect", label: i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.name }), icon: <Search />, onClick: () => setViewing(row) },
|
|
||||||
{ id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: <Pencil />, disabled: !canUpdate, onClick: () => openEdit(row) },
|
|
||||||
{ id: "suspend", label: i18nMessage("i18n:govoplan-access.suspend_value.03a74b32", { value0: row.name }), icon: <Trash2 />, variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.id === activeTenantId, onClick: () => setConfirmSuspend(row) }
|
|
||||||
]} /> }],
|
|
||||||
[activeTenantId, canSuspend, canUpdate]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AdminPageLayout
|
|
||||||
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}>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" ? "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="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>i18n:govoplan-access.system_governance_overrides.97cdf3ce</h3>
|
|
||||||
<div className="admin-form-grid two-columns">
|
|
||||||
<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">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="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>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="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">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);
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ const translations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const accessAdminSurfaces = [
|
const accessAdminSurfaces = [
|
||||||
{ id: "access.admin.system-tenants", moduleId: "access", kind: "section" as const, label: "System tenants", order: 10 },
|
|
||||||
{ id: "access.admin.system-roles", moduleId: "access", kind: "section" as const, label: "System roles", order: 20 },
|
{ id: "access.admin.system-roles", moduleId: "access", kind: "section" as const, label: "System roles", order: 20 },
|
||||||
{ id: "access.admin.system-users", moduleId: "access", kind: "section" as const, label: "System users", order: 50 },
|
{ id: "access.admin.system-users", moduleId: "access", kind: "section" as const, label: "System users", order: 50 },
|
||||||
{ id: "access.admin.system-credentials", moduleId: "access", kind: "section" as const, label: "System credentials", order: 80 },
|
{ id: "access.admin.system-credentials", moduleId: "access", kind: "section" as const, label: "System credentials", order: 80 },
|
||||||
@@ -21,7 +20,6 @@ const accessAdminSurfaces = [
|
|||||||
{ id: "access.admin.tenant-users", moduleId: "access", kind: "section" as const, label: "Tenant users", order: 40 },
|
{ id: "access.admin.tenant-users", moduleId: "access", kind: "section" as const, label: "Tenant users", order: 40 },
|
||||||
{ id: "access.admin.tenant-credentials", moduleId: "access", kind: "section" as const, label: "Tenant credentials", order: 70 },
|
{ id: "access.admin.tenant-credentials", moduleId: "access", kind: "section" as const, label: "Tenant credentials", order: 70 },
|
||||||
{ id: "access.admin.tenant-api-keys", moduleId: "access", kind: "section" as const, label: "Tenant API keys", order: 80 },
|
{ id: "access.admin.tenant-api-keys", moduleId: "access", kind: "section" as const, label: "Tenant API keys", order: 80 },
|
||||||
{ id: "access.admin.tenant-settings", moduleId: "access", kind: "section" as const, label: "Tenant settings", order: 90 },
|
|
||||||
{ id: "access.admin.group-credentials", moduleId: "access", kind: "section" as const, label: "Group credentials", order: 30 },
|
{ id: "access.admin.group-credentials", moduleId: "access", kind: "section" as const, label: "Group credentials", order: 30 },
|
||||||
{ id: "access.admin.user-credentials", moduleId: "access", kind: "section" as const, label: "User credentials", order: 30 },
|
{ id: "access.admin.user-credentials", moduleId: "access", kind: "section" as const, label: "User credentials", order: 30 },
|
||||||
{ id: "access.settings.credentials", moduleId: "access", kind: "section" as const, label: "Personal credentials", order: 30 }
|
{ id: "access.settings.credentials", moduleId: "access", kind: "section" as const, label: "Personal credentials", order: 30 }
|
||||||
|
|||||||
Reference in New Issue
Block a user