Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9e1fb287f | |||
| 11ecf362a3 |
20
README.md
20
README.md
@@ -1,5 +1,9 @@
|
||||
# GovOPlaN Admin
|
||||
|
||||
<!-- govoplan-repository-type:start -->
|
||||
**Repository type:** module (platform).
|
||||
<!-- govoplan-repository-type:end -->
|
||||
|
||||
`govoplan-admin` owns generic system administration API and WebUI contributions
|
||||
during the GovOPlaN module split.
|
||||
|
||||
@@ -43,3 +47,19 @@ Package mutation is intentionally not executed inside the FastAPI request. The
|
||||
admin UI records operator intent and queues or renders commands for the trusted
|
||||
installer process described in
|
||||
`/mnt/DATA/git/govoplan-core/docs/MODULE_ARCHITECTURE.md`.
|
||||
|
||||
## Package Surfaces
|
||||
|
||||
The admin UI intentionally exposes two different package concepts:
|
||||
|
||||
- **Configuration packages** are import/export bundles for module-owned
|
||||
configuration data. They support dry-run diagnostics, approval, apply, and
|
||||
export workflows without installing Python or WebUI packages.
|
||||
- **Module package catalog** and **operator install plan** live under
|
||||
**Modules**. They describe approved release artifacts, install/update/remove
|
||||
plans, preflight blockers, maintenance-mode requirements, installer-daemon
|
||||
requests, and rollback visibility.
|
||||
|
||||
These surfaces should stay separate in navigation and copy. If future UI work
|
||||
combines them visually, it must still preserve the operator distinction between
|
||||
configuration mutation and package installation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/admin-webui",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "webui/src/index.ts",
|
||||
@@ -18,7 +18,7 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.7",
|
||||
"@govoplan/core-webui": "^0.1.8",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-admin"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
description = "GovOPlaN generic administration module."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = [
|
||||
"govoplan-core>=0.1.7",
|
||||
"govoplan-access>=0.1.7",
|
||||
"govoplan-core>=0.1.8",
|
||||
"govoplan-access>=0.1.8",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -59,6 +59,12 @@ from govoplan_core.core.module_installer import (
|
||||
read_module_installer_request,
|
||||
retry_module_installer_request,
|
||||
)
|
||||
from govoplan_core.core.module_installer_notifications import (
|
||||
emit_module_installer_notification,
|
||||
installer_notification_body,
|
||||
installer_notification_priority,
|
||||
installer_notification_subject,
|
||||
)
|
||||
from govoplan_core.core.module_license import module_license_decision, module_license_diagnostics
|
||||
from govoplan_core.core.module_package_catalog import record_module_package_catalog_acceptance, validate_module_package_catalog
|
||||
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, MaintenanceMode, saved_maintenance_mode, save_maintenance_mode
|
||||
@@ -147,6 +153,11 @@ def _request_registry(request: Request) -> PlatformRegistry:
|
||||
return registry
|
||||
|
||||
|
||||
def _optional_request_registry(request: Request) -> PlatformRegistry | None:
|
||||
registry = getattr(request.app.state, "govoplan_registry", None)
|
||||
return registry if isinstance(registry, PlatformRegistry) else None
|
||||
|
||||
|
||||
def _request_lifecycle(request: Request) -> ModuleLifecycleManager:
|
||||
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
|
||||
if not isinstance(lifecycle, ModuleLifecycleManager):
|
||||
@@ -154,6 +165,29 @@ def _request_lifecycle(request: Request) -> ModuleLifecycleManager:
|
||||
return lifecycle
|
||||
|
||||
|
||||
def _emit_installer_request_notification(
|
||||
*,
|
||||
session: Session,
|
||||
registry: PlatformRegistry | None,
|
||||
tenant_id: str | None,
|
||||
request: dict[str, object],
|
||||
event_kind: str,
|
||||
recipient_id: str | None = None,
|
||||
) -> None:
|
||||
status_value = str(request.get("status") or event_kind.rsplit(".", 1)[-1])
|
||||
emit_module_installer_notification(
|
||||
session=session,
|
||||
registry=registry,
|
||||
tenant_id=tenant_id,
|
||||
request=request,
|
||||
event_kind=event_kind,
|
||||
subject=installer_notification_subject(event_kind, request),
|
||||
body_text=installer_notification_body(event_kind, request),
|
||||
recipient_id=recipient_id,
|
||||
priority=installer_notification_priority(status_value),
|
||||
)
|
||||
|
||||
|
||||
def _http_admin_error(exc: Exception) -> HTTPException:
|
||||
if isinstance(exc, AdminConflictError):
|
||||
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
|
||||
@@ -663,6 +697,7 @@ def read_module_install_request_detail(
|
||||
@router.post("/system/modules/install-requests", response_model=ModuleInstallerRequestItem)
|
||||
def create_module_install_request(
|
||||
payload: ModuleInstallerRequestCreateRequest,
|
||||
request_context: Request,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
|
||||
):
|
||||
@@ -675,8 +710,17 @@ def create_module_install_request(
|
||||
request = queue_module_installer_request(
|
||||
runtime_dir=runtime_dir,
|
||||
requested_by=principal.user.id,
|
||||
tenant_id=principal.tenant_id,
|
||||
options=payload.options.model_dump(exclude_none=True),
|
||||
)
|
||||
_emit_installer_request_notification(
|
||||
session=session,
|
||||
registry=_optional_request_registry(request_context),
|
||||
tenant_id=principal.tenant_id,
|
||||
request=request,
|
||||
event_kind="module_installer.request.queued",
|
||||
recipient_id=principal.user.id,
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
@@ -698,6 +742,7 @@ def create_module_install_request(
|
||||
@router.post("/system/modules/install-requests/{request_id}/cancel", response_model=ModuleInstallerRequestItem)
|
||||
def cancel_module_install_request(
|
||||
request_id: str,
|
||||
request_context: Request,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
|
||||
):
|
||||
@@ -715,6 +760,14 @@ def cancel_module_install_request(
|
||||
)
|
||||
except ModuleInstallerError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
_emit_installer_request_notification(
|
||||
session=session,
|
||||
registry=_optional_request_registry(request_context),
|
||||
tenant_id=str(request.get("tenant_id") or principal.tenant_id),
|
||||
request=request,
|
||||
event_kind="module_installer.request.cancelled",
|
||||
recipient_id=str(request.get("requested_by") or principal.user.id),
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
@@ -736,6 +789,7 @@ def cancel_module_install_request(
|
||||
@router.post("/system/modules/install-requests/{request_id}/retry", response_model=ModuleInstallerRequestItem)
|
||||
def retry_module_install_request(
|
||||
request_id: str,
|
||||
request_context: Request,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
|
||||
):
|
||||
@@ -753,6 +807,14 @@ def retry_module_install_request(
|
||||
)
|
||||
except ModuleInstallerError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
_emit_installer_request_notification(
|
||||
session=session,
|
||||
registry=_optional_request_registry(request_context),
|
||||
tenant_id=str(request.get("tenant_id") or principal.tenant_id),
|
||||
request=request,
|
||||
event_kind="module_installer.request.queued",
|
||||
recipient_id=principal.user.id,
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
|
||||
@@ -3,61 +3,10 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
|
||||
|
||||
RETENTION_DAY_KEYS = (
|
||||
"raw_campaign_json_retention_days",
|
||||
"generated_eml_retention_days",
|
||||
"stored_report_detail_retention_days",
|
||||
"mock_mailbox_retention_days",
|
||||
"audit_detail_retention_days",
|
||||
)
|
||||
|
||||
|
||||
RETENTION_POLICY_FIELD_KEYS = (
|
||||
"store_raw_campaign_json",
|
||||
*RETENTION_DAY_KEYS,
|
||||
"audit_detail_level",
|
||||
)
|
||||
|
||||
|
||||
def default_allow_lower_level_limits() -> dict[str, bool]:
|
||||
return {key: True for key in RETENTION_POLICY_FIELD_KEYS}
|
||||
|
||||
|
||||
def normalize_allow_lower_level_limits(value: Any, *, fill_defaults: bool) -> dict[str, bool] | None:
|
||||
if value in (None, ""):
|
||||
return default_allow_lower_level_limits() if fill_defaults else None
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("allow_lower_level_limits must be an object")
|
||||
normalized = default_allow_lower_level_limits() if fill_defaults else {}
|
||||
for key, allowed in value.items():
|
||||
clean_key = str(key)
|
||||
if clean_key not in RETENTION_POLICY_FIELD_KEYS:
|
||||
raise ValueError(f"Unknown retention policy field: {clean_key}")
|
||||
normalized[clean_key] = bool(allowed)
|
||||
return normalized
|
||||
|
||||
|
||||
class PrivacyRetentionPolicyItem(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
store_raw_campaign_json: bool = True
|
||||
raw_campaign_json_retention_days: int | None = Field(default=None, ge=0)
|
||||
generated_eml_retention_days: int | None = Field(default=None, ge=0)
|
||||
stored_report_detail_retention_days: int | None = Field(default=None, ge=0)
|
||||
mock_mailbox_retention_days: int | None = Field(default=None, ge=0)
|
||||
audit_detail_retention_days: int | None = Field(default=None, ge=0)
|
||||
audit_detail_level: Literal["full", "redacted", "minimal"] = "full"
|
||||
allow_lower_level_limits: dict[str, bool] = Field(default_factory=default_allow_lower_level_limits)
|
||||
|
||||
@field_validator("allow_lower_level_limits", mode="before")
|
||||
@classmethod
|
||||
def _normalize_allow_lower_level_limits(cls, value: Any) -> Any:
|
||||
return normalize_allow_lower_level_limits(value, fill_defaults=True)
|
||||
from govoplan_core.privacy.schemas import PrivacyRetentionPolicyItem
|
||||
|
||||
|
||||
class MaintenanceModeItem(BaseModel):
|
||||
|
||||
@@ -17,7 +17,7 @@ def _route_factory(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="admin",
|
||||
name="Admin",
|
||||
version="0.1.7",
|
||||
version="0.1.8",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
route_factory=_route_factory,
|
||||
migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/admin-webui",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.7",
|
||||
"@govoplan/core-webui": "^0.1.8",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -1,74 +1,16 @@
|
||||
import type { ApiSettings, DeltaDeletedItem } from "@govoplan/core-webui";
|
||||
import { apiFetch } from "@govoplan/core-webui";
|
||||
|
||||
export type PermissionItem = {
|
||||
scope: string;
|
||||
label: string;
|
||||
description: string;
|
||||
category: string;
|
||||
level: "tenant" | "system";
|
||||
system_template_id?: string | null;
|
||||
system_required?: boolean;
|
||||
};
|
||||
|
||||
export type AdminOverview = {
|
||||
active_tenant_id: string;
|
||||
active_tenant_name: string;
|
||||
tenant_count?: number | null;
|
||||
system_account_count?: number | null;
|
||||
system_group_template_count?: number | null;
|
||||
system_role_template_count?: number | null;
|
||||
user_count: number;
|
||||
active_user_count: number;
|
||||
group_count: number;
|
||||
role_count: number;
|
||||
active_api_key_count: number;
|
||||
capabilities: string[];
|
||||
};
|
||||
|
||||
export type TenantAdminItem = {
|
||||
id: string;
|
||||
slug: string;
|
||||
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;
|
||||
counts: Record<string, number>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type PrivacyRetentionPolicyFieldKey =
|
||||
| "store_raw_campaign_json"
|
||||
| "raw_campaign_json_retention_days"
|
||||
| "generated_eml_retention_days"
|
||||
| "stored_report_detail_retention_days"
|
||||
| "mock_mailbox_retention_days"
|
||||
| "audit_detail_retention_days"
|
||||
| "audit_detail_level";
|
||||
|
||||
export type PrivacyRetentionLimitPermissions = Record<PrivacyRetentionPolicyFieldKey, boolean>;
|
||||
export type PrivacyRetentionLimitPermissionPatch = Partial<PrivacyRetentionLimitPermissions>;
|
||||
|
||||
export type PrivacyRetentionPolicy = {
|
||||
store_raw_campaign_json: boolean;
|
||||
raw_campaign_json_retention_days?: number | null;
|
||||
generated_eml_retention_days?: number | null;
|
||||
stored_report_detail_retention_days?: number | null;
|
||||
mock_mailbox_retention_days?: number | null;
|
||||
audit_detail_retention_days?: number | null;
|
||||
audit_detail_level: "full" | "redacted" | "minimal";
|
||||
allow_lower_level_limits: PrivacyRetentionLimitPermissions;
|
||||
};
|
||||
|
||||
export type PrivacyRetentionPolicyPatch = Partial<Omit<PrivacyRetentionPolicy, "allow_lower_level_limits">> & {
|
||||
allow_lower_level_limits?: PrivacyRetentionLimitPermissionPatch;
|
||||
};
|
||||
import type { ApiSettings, DeltaDeletedItem, PrivacyRetentionPolicy } from "@govoplan/core-webui";
|
||||
import { apiFetch, apiGetList, apiPath, apiQuery } from "@govoplan/core-webui";
|
||||
export { fetchAdminOverview, fetchPermissionCatalog, fetchTenants } from "@govoplan/core-webui";
|
||||
export type {
|
||||
AdminOverview,
|
||||
PrivacyRetentionLimitPermissionPatch,
|
||||
PrivacyRetentionLimitPermissions,
|
||||
PrivacyRetentionPolicy,
|
||||
PrivacyRetentionPolicyFieldKey,
|
||||
PrivacyRetentionPolicyPatch,
|
||||
PermissionItem,
|
||||
TenantAdminItem
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
export type MaintenanceMode = {
|
||||
enabled: boolean;
|
||||
@@ -192,10 +134,7 @@ type DeltaResponseFields = {
|
||||
};
|
||||
|
||||
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
|
||||
const params = new URLSearchParams();
|
||||
if (options.since) params.set("since", options.since);
|
||||
if (options.limit) params.set("limit", String(options.limit));
|
||||
return params.toString() ? `?${params.toString()}` : "";
|
||||
return apiQuery(options);
|
||||
}
|
||||
|
||||
export type SystemSettingsDeltaResponse = {
|
||||
@@ -541,20 +480,6 @@ export type GovernanceTemplateItem = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export function fetchAdminOverview(settings: ApiSettings): Promise<AdminOverview> {
|
||||
return apiFetch(settings, "/api/v1/admin/overview");
|
||||
}
|
||||
|
||||
export async function fetchPermissionCatalog(settings: ApiSettings): Promise<PermissionItem[]> {
|
||||
const response = await apiFetch<{ permissions: PermissionItem[] }>(settings, "/api/v1/admin/permissions");
|
||||
return response.permissions;
|
||||
}
|
||||
|
||||
export async function fetchTenants(settings: ApiSettings): Promise<TenantAdminItem[]> {
|
||||
const response = await apiFetch<{ tenants: TenantAdminItem[] }>(settings, "/api/v1/admin/tenants");
|
||||
return response.tenants;
|
||||
}
|
||||
|
||||
export function fetchSystemSettings(settings: ApiSettings): Promise<SystemSettingsItem> {
|
||||
return apiFetch(settings, "/api/v1/admin/system/settings");
|
||||
}
|
||||
@@ -584,19 +509,11 @@ export function fetchModuleInstallPlan(settings: ApiSettings): Promise<ModuleIns
|
||||
}
|
||||
|
||||
export function fetchModuleInstallerRuns(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRunListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.page_size) params.set("page_size", String(options.page_size));
|
||||
if (options.cursor) params.set("cursor", options.cursor);
|
||||
const suffix = params.toString() ? `?${params.toString()}` : "";
|
||||
return apiFetch(settings, `/api/v1/admin/system/modules/install-runs${suffix}`);
|
||||
return apiFetch(settings, apiPath("/api/v1/admin/system/modules/install-runs", options));
|
||||
}
|
||||
|
||||
export function fetchModuleInstallerRequests(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRequestListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.page_size) params.set("page_size", String(options.page_size));
|
||||
if (options.cursor) params.set("cursor", options.cursor);
|
||||
const suffix = params.toString() ? `?${params.toString()}` : "";
|
||||
return apiFetch(settings, `/api/v1/admin/system/modules/install-requests${suffix}`);
|
||||
return apiFetch(settings, apiPath("/api/v1/admin/system/modules/install-requests", options));
|
||||
}
|
||||
|
||||
export function createModuleInstallerRequest(settings: ApiSettings, options: ModuleInstallerRequestOptions): Promise<ModuleInstallerRequestItem> {
|
||||
@@ -638,9 +555,7 @@ export function clearModuleInstallPlan(settings: ApiSettings): Promise<ModuleIns
|
||||
}
|
||||
|
||||
export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "group" | "role"): Promise<GovernanceTemplateItem[]> {
|
||||
const suffix = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
||||
const response = await apiFetch<{ templates: GovernanceTemplateItem[] }>(settings, `/api/v1/admin/system/governance-templates${suffix}`);
|
||||
return response.templates;
|
||||
return apiGetList<GovernanceTemplateItem, "templates">(settings, "/api/v1/admin/system/governance-templates", "templates", { kind });
|
||||
}
|
||||
|
||||
export function createGovernanceTemplate(settings: ApiSettings, payload: Omit<GovernanceTemplateItem, "id" | "created_at" | "updated_at" | "effective_permission_count"> & { change_request_id?: string | null }): Promise<GovernanceTemplateItem> {
|
||||
@@ -652,8 +567,7 @@ export function updateGovernanceTemplate(settings: ApiSettings, templateId: stri
|
||||
}
|
||||
|
||||
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string, changeRequestId?: string | null): Promise<void> {
|
||||
const suffix = changeRequestId ? `?change_request_id=${encodeURIComponent(changeRequestId)}` : "";
|
||||
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}${suffix}`, { method: "DELETE" });
|
||||
return apiFetch(settings, apiPath(`/api/v1/admin/system/governance-templates/${templateId}`, { change_request_id: changeRequestId }), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> {
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
|
||||
{hasAnySection(availableSections, platformSectionIds) && <Card title="ADMINISTRATION">
|
||||
<div className="admin-overview-grid">
|
||||
{availableSections.has("system-modules") && <AreaLink title="i18n:govoplan-admin.modules.04e9462c" text="i18n:govoplan-admin.installed_modules_runtime_state_and_startup_stat.38dd7028" onClick={() => onSelect("system-modules")} />}
|
||||
{availableSections.has("system-configuration-packages") && <AreaLink title="i18n:govoplan-admin.packages.0a999012" text="i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8" onClick={() => onSelect("system-configuration-packages")} />}
|
||||
{availableSections.has("system-configuration-packages") && <AreaLink title="i18n:govoplan-admin.configuration_packages.eb2f05f1" text="i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8" onClick={() => onSelect("system-configuration-packages")} />}
|
||||
{availableSections.has("system-settings") && <AreaLink title="i18n:govoplan-admin.maintenance.94de303b" text="i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa" onClick={() => onSelect("system-settings")} />}
|
||||
{availableSections.has("system-configuration-changes") && <AreaLink title="i18n:govoplan-admin.changes.8aa57de6" text="i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335" onClick={() => onSelect("system-configuration-changes")} />}
|
||||
{availableSections.has("system-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type PermissionItem,
|
||||
type TenantAdminItem } from
|
||||
"../../api/admin";
|
||||
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
|
||||
const emptyDraft = {
|
||||
slug: "",
|
||||
@@ -54,6 +54,7 @@ export default function GovernanceTemplatesPanel({
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const dirty = editing !== null && draftKey(draft) !== savedDraftKey;
|
||||
const permissionsByScope = useMemo(() => new Map(permissions.map((permission) => [permission.scope, permission])), [permissions]);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
@@ -238,7 +239,7 @@ export default function GovernanceTemplatesPanel({
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={Boolean(viewing)} title={viewing?.name || "i18n:govoplan-admin.template_details.d5d75e4d"} onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-admin.close.bbfa773e</Button>}>
|
||||
{viewing && <dl className="admin-details-grid"><div><dt>i18n:govoplan-admin.kind.e00ac23f</dt><dd>{viewing.kind}</dd></div><div><dt>i18n:govoplan-admin.slug.094da9b9</dt><dd>{viewing.slug}</dd></div><div><dt>i18n:govoplan-admin.status.bae7d5be</dt><dd>{viewing.is_active ? "i18n:govoplan-admin.active.a733b809" : "i18n:govoplan-admin.inactive.09af574c"}</dd></div><div><dt>i18n:govoplan-admin.tenants.1f7ae776</dt><dd>{viewing.assignments.length || "i18n:govoplan-admin.none.6eef6648"}</dd></div><div><dt>i18n:govoplan-admin.description.55f8ebc8</dt><dd>{viewing.description || "—"}</dd></div><div><dt>i18n:govoplan-admin.permissions.d06d5557</dt><dd>{viewing.permissions.length ? joinLabels(viewing.permissions.map((name) => ({ name }))) : "—"}</dd></div></dl>}
|
||||
{viewing && <dl className="admin-details-grid"><div><dt>i18n:govoplan-admin.kind.e00ac23f</dt><dd>{viewing.kind}</dd></div><div><dt>i18n:govoplan-admin.slug.094da9b9</dt><dd>{viewing.slug}</dd></div><div><dt>i18n:govoplan-admin.status.bae7d5be</dt><dd>{viewing.is_active ? "i18n:govoplan-admin.active.a733b809" : "i18n:govoplan-admin.inactive.09af574c"}</dd></div><div><dt>i18n:govoplan-admin.tenants.1f7ae776</dt><dd>{viewing.assignments.length || "i18n:govoplan-admin.none.6eef6648"}</dd></div><div><dt>i18n:govoplan-admin.description.55f8ebc8</dt><dd>{viewing.description || "—"}</dd></div><div><dt>i18n:govoplan-admin.permissions.d06d5557</dt><dd>{viewing.permissions.length ? <PermissionDetails scopes={viewing.permissions} permissionsByScope={permissionsByScope} /> : "—"}</dd></div></dl>}
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog open={Boolean(deleting)} title={kind === "group" ? "i18n:govoplan-admin.delete_group_template.8745d842" : "i18n:govoplan-admin.delete_tenant_role.4efa0813"} message={i18nMessage("i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d", { value0: deleting?.name })} confirmLabel={kind === "group" ? "i18n:govoplan-admin.delete_template.399bf72a" : "i18n:govoplan-admin.delete_tenant_role.4efa0813"} tone="danger" busy={busy} onCancel={() => setDeleting(null)} onConfirm={() => void remove()} />
|
||||
@@ -249,3 +250,31 @@ export default function GovernanceTemplatesPanel({
|
||||
function draftKey(draft: typeof emptyDraft): string {
|
||||
return JSON.stringify(draft);
|
||||
}
|
||||
|
||||
function PermissionDetails({ scopes, permissionsByScope }: {scopes: string[];permissionsByScope: ReadonlyMap<string, PermissionItem>;}) {
|
||||
const groups = groupPermissionScopes(scopes, permissionsByScope);
|
||||
return (
|
||||
<div className="admin-permission-details">
|
||||
{groups.map((group) => <section key={group.module}>
|
||||
<strong>{group.module}</strong>
|
||||
<ul>
|
||||
{group.permissions.map((permission) => <li key={permission.scope}>
|
||||
<span>{permission.label}</span>
|
||||
<code>{permission.scope}</code>
|
||||
</li>)}
|
||||
</ul>
|
||||
</section>)}
|
||||
</div>);
|
||||
}
|
||||
|
||||
function groupPermissionScopes(scopes: string[], permissionsByScope: ReadonlyMap<string, PermissionItem>) {
|
||||
const groups = new Map<string, {scope: string;label: string}[]>();
|
||||
for (const scope of [...scopes].sort()) {
|
||||
const moduleId = scope.split(":", 1)[0] || "other";
|
||||
const permission = permissionsByScope.get(scope);
|
||||
const group = groups.get(moduleId) ?? [];
|
||||
group.push({ scope, label: permission?.label || scope });
|
||||
groups.set(moduleId, group);
|
||||
}
|
||||
return [...groups.entries()].map(([module, permissions]) => ({ module, permissions }));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.after.695c89be": "After:",
|
||||
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
|
||||
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
|
||||
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add tenant role",
|
||||
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add role template",
|
||||
"i18n:govoplan-admin.admin.4e7afebc": "Admin",
|
||||
"i18n:govoplan-admin.administration.b8be3d12": "Administration",
|
||||
"i18n:govoplan-admin.platform_administration": "Platform administration",
|
||||
@@ -75,7 +75,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.create_group_template.72407248": "Create group template",
|
||||
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply",
|
||||
"i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39": "Create, suspend and govern tenant spaces.",
|
||||
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create tenant role",
|
||||
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create role template",
|
||||
"i18n:govoplan-admin.created.0c78dab1": "Created:",
|
||||
"i18n:govoplan-admin.created.accf40c8": "Created",
|
||||
"i18n:govoplan-admin.current_window.73e25c9f": "Current window:",
|
||||
@@ -100,7 +100,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Defaults for newly created tenants",
|
||||
"i18n:govoplan-admin.delete_group_template.8745d842": "Delete group template",
|
||||
"i18n:govoplan-admin.delete_template.399bf72a": "Delete template",
|
||||
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete tenant role",
|
||||
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete role template",
|
||||
"i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d": "Delete {value0}? Removal is blocked while a materialized tenant definition still has members or assignments.",
|
||||
"i18n:govoplan-admin.delete_value.4d18989e": "Delete {value0}",
|
||||
"i18n:govoplan-admin.dependencies.9f4f78d1": "Dependencies:",
|
||||
@@ -115,7 +115,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07": "Dry-run change request created: {value0}. Enable maintenance mode, then apply the request.",
|
||||
"i18n:govoplan-admin.dry_run.3d14659c": "Dry-run",
|
||||
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template",
|
||||
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit tenant role",
|
||||
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit role template",
|
||||
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
|
||||
"i18n:govoplan-admin.enabled_modules.3c38e9ff": "Enabled modules:",
|
||||
@@ -302,7 +302,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.save_plan.842dc280": "Save plan",
|
||||
"i18n:govoplan-admin.save_settings.913aba9f": "Save settings",
|
||||
"i18n:govoplan-admin.save_template.0885fab2": "Save template",
|
||||
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save tenant role",
|
||||
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save role template",
|
||||
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Save the install plan before queueing a daemon request.",
|
||||
"i18n:govoplan-admin.saved.c0ae8f6e": "Saved",
|
||||
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
|
||||
@@ -347,9 +347,9 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b": "Tenant locale and tenant-specific settings.",
|
||||
"i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6": "Tenant memberships and inherited roles.",
|
||||
"i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea": "Tenant permission bundles and system-managed role copies.",
|
||||
"i18n:govoplan-admin.tenant_permissions.246294bc": "Tenant permissions",
|
||||
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role",
|
||||
"i18n:govoplan-admin.tenant_roles.51aca82d": "Tenant roles",
|
||||
"i18n:govoplan-admin.tenant_permissions.246294bc": "Permissions",
|
||||
"i18n:govoplan-admin.tenant_role.6b53115d": "Role template",
|
||||
"i18n:govoplan-admin.tenant_roles.51aca82d": "Role templates",
|
||||
"i18n:govoplan-admin.tenant_users.cb800b38": "Tenant users",
|
||||
"i18n:govoplan-admin.tenants.1f7ae776": "Tenants",
|
||||
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "These settings are enforced by the backend. Central groups and tenant roles remain available even when local creation is disabled.",
|
||||
@@ -403,7 +403,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.after.695c89be": "Nach:",
|
||||
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
|
||||
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
|
||||
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add tenant role",
|
||||
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Rollenvorlage hinzufügen",
|
||||
"i18n:govoplan-admin.admin.4e7afebc": "Administration",
|
||||
"i18n:govoplan-admin.administration.b8be3d12": "Administration",
|
||||
"i18n:govoplan-admin.platform_administration": "Plattformadministration",
|
||||
@@ -464,7 +464,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.create_group_template.72407248": "Create group template",
|
||||
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply",
|
||||
"i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39": "Create, suspend and govern tenant spaces.",
|
||||
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create tenant role",
|
||||
"i18n:govoplan-admin.create_tenant_role.f58db104": "Rollenvorlage erstellen",
|
||||
"i18n:govoplan-admin.created.0c78dab1": "Created:",
|
||||
"i18n:govoplan-admin.created.accf40c8": "Erstellt",
|
||||
"i18n:govoplan-admin.current_window.73e25c9f": "Aktuelles Fenster:",
|
||||
@@ -489,7 +489,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten",
|
||||
"i18n:govoplan-admin.delete_group_template.8745d842": "Delete group template",
|
||||
"i18n:govoplan-admin.delete_template.399bf72a": "Delete template",
|
||||
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete tenant role",
|
||||
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Rollenvorlage löschen",
|
||||
"i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d": "Delete {value0}? Removal is blocked while a materialized tenant definition still has members or assignments.",
|
||||
"i18n:govoplan-admin.delete_value.4d18989e": "Delete {value0}",
|
||||
"i18n:govoplan-admin.dependencies.9f4f78d1": "Abhaengigkeiten:",
|
||||
@@ -504,7 +504,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07": "Dry-run change request created: {value0}. Enable maintenance mode, then apply the request.",
|
||||
"i18n:govoplan-admin.dry_run.3d14659c": "Dry-run",
|
||||
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template",
|
||||
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit tenant role",
|
||||
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Rollenvorlage bearbeiten",
|
||||
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
|
||||
"i18n:govoplan-admin.enabled_modules.3c38e9ff": "Aktive Module:",
|
||||
@@ -691,7 +691,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.save_plan.842dc280": "Save plan",
|
||||
"i18n:govoplan-admin.save_settings.913aba9f": "Einstellungen speichern",
|
||||
"i18n:govoplan-admin.save_template.0885fab2": "Save template",
|
||||
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save tenant role",
|
||||
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Rollenvorlage speichern",
|
||||
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Speichern Sie den Installationsplan, bevor eine Daemon-Anfrage eingereiht wird.",
|
||||
"i18n:govoplan-admin.saved.c0ae8f6e": "Gespeichert",
|
||||
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
|
||||
@@ -736,9 +736,9 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b": "Tenant locale and tenant-specific settings.",
|
||||
"i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6": "Tenant memberships and inherited roles.",
|
||||
"i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea": "Tenant permission bundles and system-managed role copies.",
|
||||
"i18n:govoplan-admin.tenant_permissions.246294bc": "Tenant permissions",
|
||||
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role",
|
||||
"i18n:govoplan-admin.tenant_roles.51aca82d": "Mandantenrollen",
|
||||
"i18n:govoplan-admin.tenant_permissions.246294bc": "Berechtigungen",
|
||||
"i18n:govoplan-admin.tenant_role.6b53115d": "Rollenvorlage",
|
||||
"i18n:govoplan-admin.tenant_roles.51aca82d": "Rollenvorlagen",
|
||||
"i18n:govoplan-admin.tenant_users.cb800b38": "Mandantenbenutzer",
|
||||
"i18n:govoplan-admin.tenants.1f7ae776": "Mandanten",
|
||||
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "Diese Einstellungen werden vom Backend erzwungen. Zentrale Gruppen und Mandantenrollen bleiben verfügbar, auch wenn lokale Erstellung deaktiviert ist.",
|
||||
|
||||
@@ -54,7 +54,7 @@ const adminSections: AdminSectionsUiCapability = {
|
||||
},
|
||||
{
|
||||
id: "system-configuration-packages",
|
||||
label: "i18n:govoplan-admin.packages.0a999012",
|
||||
label: "i18n:govoplan-admin.configuration_packages.eb2f05f1",
|
||||
group: "SYSTEM",
|
||||
order: 30,
|
||||
allOf: ["system:settings:read"],
|
||||
|
||||
Reference in New Issue
Block a user