9 Commits
v0.1.7 ... main

18 changed files with 488 additions and 377 deletions

View File

@@ -1,5 +1,9 @@
# GovOPlaN Admin # GovOPlaN Admin
<!-- govoplan-repository-type:start -->
**Repository type:** module (platform).
<!-- govoplan-repository-type:end -->
`govoplan-admin` owns generic system administration API and WebUI contributions `govoplan-admin` owns generic system administration API and WebUI contributions
during the GovOPlaN module split. 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 admin UI records operator intent and queues or renders commands for the trusted
installer process described in installer process described in
`/mnt/DATA/git/govoplan-core/docs/MODULE_ARCHITECTURE.md`. `/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.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/admin-webui", "name": "@govoplan/admin-webui",
"version": "0.1.7", "version": "0.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "webui/src/index.ts", "main": "webui/src/index.ts",
@@ -18,7 +18,7 @@
"LICENSE" "LICENSE"
], ],
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.7", "@govoplan/core-webui": "^0.1.9",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-admin" name = "govoplan-admin"
version = "0.1.7" version = "0.1.8"
description = "GovOPlaN generic administration module." description = "GovOPlaN generic administration module."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = [ dependencies = [
"govoplan-core>=0.1.7", "govoplan-core>=0.1.8",
"govoplan-access>=0.1.7", "govoplan-access>=0.1.8",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]

View File

@@ -59,6 +59,12 @@ from govoplan_core.core.module_installer import (
read_module_installer_request, read_module_installer_request,
retry_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_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.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 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 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: def _request_lifecycle(request: Request) -> ModuleLifecycleManager:
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None) lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
if not isinstance(lifecycle, ModuleLifecycleManager): if not isinstance(lifecycle, ModuleLifecycleManager):
@@ -154,6 +165,29 @@ def _request_lifecycle(request: Request) -> ModuleLifecycleManager:
return lifecycle 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: def _http_admin_error(exc: Exception) -> HTTPException:
if isinstance(exc, AdminConflictError): if isinstance(exc, AdminConflictError):
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
@@ -452,42 +486,73 @@ def _catalog_plan_item(
validation: dict[str, object] | None = None, validation: dict[str, object] | None = None,
) -> tuple[ModuleInstallPlanItem, dict[str, object]]: ) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
result = validation or validate_module_package_catalog() result = validation or validate_module_package_catalog()
_require_valid_module_catalog(result)
raw_item = _catalog_install_or_update_item(result, module_id)
action = _catalog_plan_action(raw_item, module_id, available_module_ids)
license_decision = module_license_decision(_catalog_license_features(raw_item))
_require_catalog_action_license(license_decision, action=action, module_id=module_id)
return ModuleInstallPlanItem(
module_id=str(raw_item["module_id"]),
action=action,
source="catalog",
catalog=_catalog_plan_metadata(result),
python_package=_catalog_string(raw_item, "python_package"),
python_ref=_catalog_string(raw_item, "python_ref"),
webui_package=_catalog_string(raw_item, "webui_package"),
webui_ref=_catalog_string(raw_item, "webui_ref"),
notes=_catalog_plan_notes(raw_item, license_decision),
), result
def _require_valid_module_catalog(result: dict[str, object]) -> None:
if not result.get("valid"): if not result.get("valid"):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(result.get("error") or "Module package catalog is invalid."), detail=str(result.get("error") or "Module package catalog is invalid."),
) )
def _catalog_install_or_update_item(result: dict[str, object], module_id: str) -> dict[str, object]:
for raw_item in result.get("modules", []): for raw_item in result.get("modules", []):
if not isinstance(raw_item, dict): if not isinstance(raw_item, dict):
continue continue
if raw_item.get("module_id") != module_id or raw_item.get("action") not in {"install", "update"}: if raw_item.get("module_id") != module_id or raw_item.get("action") not in {"install", "update"}:
continue continue
raw_action = raw_item.get("action") return raw_item
action = "update" if raw_action == "update" or module_id in available_module_ids else "install" raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Catalog install/update entry not found: {module_id}")
license_decision = module_license_decision(_catalog_license_features(raw_item))
if not license_decision.get("allowed"):
def _catalog_plan_action(raw_item: dict[str, object], module_id: str, available_module_ids: set[str]) -> str:
return "update" if raw_item.get("action") == "update" or module_id in available_module_ids else "install"
def _require_catalog_action_license(
license_decision: dict[str, object],
*,
action: str,
module_id: str,
) -> None:
if license_decision.get("allowed"):
return
missing = license_decision.get("missing_features") missing = license_decision.get("missing_features")
missing_text = ", ".join(str(item) for item in missing) if isinstance(missing, list) else "required feature" missing_text = ", ".join(str(item) for item in missing) if isinstance(missing, list) else "required feature"
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail=f"License does not allow {action} for {module_id}: {missing_text}.", detail=f"License does not allow {action} for {module_id}: {missing_text}.",
) )
notes = raw_item.get("notes") if isinstance(raw_item.get("notes"), str) else None
if license_decision.get("reason") and license_decision.get("missing_features"):
def _catalog_plan_notes(raw_item: dict[str, object], license_decision: dict[str, object]) -> str | None:
notes = _catalog_string(raw_item, "notes")
if not (license_decision.get("reason") and license_decision.get("missing_features")):
return notes
prefix = f"{notes}\n" if notes else "" prefix = f"{notes}\n" if notes else ""
notes = f"{prefix}License warning: {license_decision['reason']}" return f"{prefix}License warning: {license_decision['reason']}"
return ModuleInstallPlanItem(
module_id=str(raw_item["module_id"]),
action=action, def _catalog_string(raw_item: dict[str, object], field: str) -> str | None:
source="catalog", value = raw_item.get(field)
catalog=_catalog_plan_metadata(result), return value if isinstance(value, str) else None
python_package=raw_item.get("python_package") if isinstance(raw_item.get("python_package"), str) else None,
python_ref=raw_item.get("python_ref") if isinstance(raw_item.get("python_ref"), str) else None,
webui_package=raw_item.get("webui_package") if isinstance(raw_item.get("webui_package"), str) else None,
webui_ref=raw_item.get("webui_ref") if isinstance(raw_item.get("webui_ref"), str) else None,
notes=notes,
), result
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Catalog install/update entry not found: {module_id}")
def _catalog_plan_metadata(validation: dict[str, object]) -> dict[str, object]: def _catalog_plan_metadata(validation: dict[str, object]) -> dict[str, object]:
@@ -663,6 +728,7 @@ def read_module_install_request_detail(
@router.post("/system/modules/install-requests", response_model=ModuleInstallerRequestItem) @router.post("/system/modules/install-requests", response_model=ModuleInstallerRequestItem)
def create_module_install_request( def create_module_install_request(
payload: ModuleInstallerRequestCreateRequest, payload: ModuleInstallerRequestCreateRequest,
request_context: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")), principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
): ):
@@ -675,8 +741,17 @@ def create_module_install_request(
request = queue_module_installer_request( request = queue_module_installer_request(
runtime_dir=runtime_dir, runtime_dir=runtime_dir,
requested_by=principal.user.id, requested_by=principal.user.id,
tenant_id=principal.tenant_id,
options=payload.options.model_dump(exclude_none=True), 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( audit_from_principal(
session, session,
principal, principal,
@@ -698,6 +773,7 @@ def create_module_install_request(
@router.post("/system/modules/install-requests/{request_id}/cancel", response_model=ModuleInstallerRequestItem) @router.post("/system/modules/install-requests/{request_id}/cancel", response_model=ModuleInstallerRequestItem)
def cancel_module_install_request( def cancel_module_install_request(
request_id: str, request_id: str,
request_context: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")), principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
): ):
@@ -715,6 +791,14 @@ def cancel_module_install_request(
) )
except ModuleInstallerError as exc: except ModuleInstallerError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from 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( audit_from_principal(
session, session,
principal, principal,
@@ -736,6 +820,7 @@ def cancel_module_install_request(
@router.post("/system/modules/install-requests/{request_id}/retry", response_model=ModuleInstallerRequestItem) @router.post("/system/modules/install-requests/{request_id}/retry", response_model=ModuleInstallerRequestItem)
def retry_module_install_request( def retry_module_install_request(
request_id: str, request_id: str,
request_context: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")), principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
): ):
@@ -753,6 +838,14 @@ def retry_module_install_request(
) )
except ModuleInstallerError as exc: except ModuleInstallerError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from 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( audit_from_principal(
session, session,
principal, principal,

View File

@@ -3,61 +3,10 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Any, Literal 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 from govoplan_core.api.v1.schemas import DeltaDeletedItem
from govoplan_core.privacy.schemas import PrivacyRetentionPolicyItem
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)
class MaintenanceModeItem(BaseModel): class MaintenanceModeItem(BaseModel):

View File

@@ -3,7 +3,8 @@ from __future__ import annotations
from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.module_guards import persistent_table_uninstall_guard from govoplan_core.core.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import MigrationSpec, ModuleContext, ModuleManifest from govoplan_core.core.modules import FrontendModule, MigrationSpec, ModuleContext, ModuleManifest
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
@@ -17,9 +18,22 @@ def _route_factory(context: ModuleContext):
manifest = ModuleManifest( manifest = ModuleManifest(
id="admin", id="admin",
name="Admin", name="Admin",
version="0.1.7", version="0.1.8",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
route_factory=_route_factory, route_factory=_route_factory,
frontend=FrontendModule(
module_id="admin",
package_name="@govoplan/admin-webui",
view_surfaces=(
ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0),
ViewSurface(id="admin.section.system-settings", module_id="admin", kind="section", label="System settings", order=10),
ViewSurface(id="admin.section.system-configuration-changes", module_id="admin", kind="section", label="Configuration changes", order=20),
ViewSurface(id="admin.section.system-configuration-packages", module_id="admin", kind="section", label="Configuration packages", order=30),
ViewSurface(id="admin.section.system-role-templates", module_id="admin", kind="section", label="Role templates", order=40),
ViewSurface(id="admin.section.system-groups", module_id="admin", kind="section", label="Group templates", order=50),
ViewSurface(id="admin.section.system-modules", module_id="admin", kind="section", label="Modules", order=85),
),
),
migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata), migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata),
uninstall_guard_providers=( uninstall_guard_providers=(
persistent_table_uninstall_guard( persistent_table_uninstall_guard(

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""GovOPlaN Admin backend tests."""

108
tests/test_catalog_plan.py Normal file
View File

@@ -0,0 +1,108 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from fastapi import HTTPException
from govoplan_admin.backend.api.v1.routes import _catalog_plan_item
class CatalogPlanItemTests(unittest.TestCase):
def test_builds_catalog_item_and_preserves_catalog_metadata(self) -> None:
validation: dict[str, object] = {
"valid": True,
"source": "https://catalog.example.test/modules.json",
"source_type": "remote",
"channel": "stable",
"signed": True,
"trusted": True,
"modules": [
"ignored",
{"module_id": "other", "action": "install"},
{
"module_id": "calendar",
"action": "install",
"python_package": "govoplan-calendar",
"python_ref": 42,
"webui_package": "@govoplan/calendar-webui",
"notes": "Catalog note",
"license_features": ["calendar.sync", "", 7],
},
],
}
decision = {
"allowed": True,
"reason": "Feature expires soon.",
"missing_features": ["calendar.future"],
}
with patch(
"govoplan_admin.backend.api.v1.routes.module_license_decision",
return_value=decision,
) as license_decision:
item, returned_validation = _catalog_plan_item(
"calendar",
{"calendar"},
validation=validation,
)
self.assertIs(returned_validation, validation)
self.assertEqual(item.module_id, "calendar")
self.assertEqual(item.action, "update")
self.assertEqual(item.source, "catalog")
self.assertEqual(item.python_package, "govoplan-calendar")
self.assertIsNone(item.python_ref)
self.assertEqual(item.webui_package, "@govoplan/calendar-webui")
self.assertIsNone(item.webui_ref)
self.assertEqual(item.notes, "Catalog note\nLicense warning: Feature expires soon.")
self.assertEqual(item.catalog["source"], "https://catalog.example.test/modules.json")
self.assertEqual(item.catalog["channel"], "stable")
self.assertTrue(item.catalog["signed"])
license_decision.assert_called_once_with(["calendar.sync", "7"])
def test_rejects_catalog_action_when_license_is_missing(self) -> None:
validation: dict[str, object] = {
"valid": True,
"modules": [{"module_id": "calendar", "action": "install", "license_features": ["calendar.sync"]}],
}
with patch(
"govoplan_admin.backend.api.v1.routes.module_license_decision",
return_value={"allowed": False, "missing_features": ["calendar.sync", "calendar.write"]},
), self.assertRaises(HTTPException) as raised:
_catalog_plan_item("calendar", set(), validation=validation)
self.assertEqual(raised.exception.status_code, 403)
self.assertEqual(
raised.exception.detail,
"License does not allow install for calendar: calendar.sync, calendar.write.",
)
def test_rejects_invalid_or_missing_catalog_entries(self) -> None:
with self.subTest("invalid catalog"), self.assertRaises(HTTPException) as invalid:
_catalog_plan_item(
"calendar",
set(),
validation={"valid": False, "error": "Signature is invalid."},
)
self.assertEqual(invalid.exception.status_code, 422)
self.assertEqual(invalid.exception.detail, "Signature is invalid.")
with self.subTest("entry not found"), self.assertRaises(HTTPException) as missing:
_catalog_plan_item(
"calendar",
set(),
validation={
"valid": True,
"modules": [
{"module_id": "calendar", "action": "remove"},
{"module_id": "other", "action": "install"},
],
},
)
self.assertEqual(missing.exception.status_code, 404)
self.assertEqual(missing.exception.detail, "Catalog install/update entry not found: calendar")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/admin-webui", "name": "@govoplan/admin-webui",
"version": "0.1.7", "version": "0.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -13,7 +13,7 @@
} }
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.7", "@govoplan/core-webui": "^0.1.9",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@@ -1,74 +1,16 @@
import type { ApiSettings, DeltaDeletedItem } from "@govoplan/core-webui"; import type { ApiSettings, DeltaDeletedItem, PrivacyRetentionPolicy } from "@govoplan/core-webui";
import { apiFetch } from "@govoplan/core-webui"; import { apiFetch, apiGetList, apiPath, apiQuery } from "@govoplan/core-webui";
export { fetchAdminOverview, fetchPermissionCatalog, fetchTenants } from "@govoplan/core-webui";
export type PermissionItem = { export type {
scope: string; AdminOverview,
label: string; PrivacyRetentionLimitPermissionPatch,
description: string; PrivacyRetentionLimitPermissions,
category: string; PrivacyRetentionPolicy,
level: "tenant" | "system"; PrivacyRetentionPolicyFieldKey,
system_template_id?: string | null; PrivacyRetentionPolicyPatch,
system_required?: boolean; PermissionItem,
}; TenantAdminItem
} from "@govoplan/core-webui";
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;
};
export type MaintenanceMode = { export type MaintenanceMode = {
enabled: boolean; enabled: boolean;
@@ -192,10 +134,7 @@ type DeltaResponseFields = {
}; };
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string { function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
const params = new URLSearchParams(); return apiQuery(options);
if (options.since) params.set("since", options.since);
if (options.limit) params.set("limit", String(options.limit));
return params.toString() ? `?${params.toString()}` : "";
} }
export type SystemSettingsDeltaResponse = { export type SystemSettingsDeltaResponse = {
@@ -541,20 +480,6 @@ export type GovernanceTemplateItem = {
updated_at: string; 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> { export function fetchSystemSettings(settings: ApiSettings): Promise<SystemSettingsItem> {
return apiFetch(settings, "/api/v1/admin/system/settings"); 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> { export function fetchModuleInstallerRuns(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRunListResponse> {
const params = new URLSearchParams(); return apiFetch(settings, apiPath("/api/v1/admin/system/modules/install-runs", options));
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}`);
} }
export function fetchModuleInstallerRequests(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRequestListResponse> { export function fetchModuleInstallerRequests(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRequestListResponse> {
const params = new URLSearchParams(); return apiFetch(settings, apiPath("/api/v1/admin/system/modules/install-requests", options));
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}`);
} }
export function createModuleInstallerRequest(settings: ApiSettings, options: ModuleInstallerRequestOptions): Promise<ModuleInstallerRequestItem> { 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[]> { export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "group" | "role"): Promise<GovernanceTemplateItem[]> {
const suffix = kind ? `?kind=${encodeURIComponent(kind)}` : ""; return apiGetList<GovernanceTemplateItem, "templates">(settings, "/api/v1/admin/system/governance-templates", "templates", { kind });
const response = await apiFetch<{ templates: GovernanceTemplateItem[] }>(settings, `/api/v1/admin/system/governance-templates${suffix}`);
return response.templates;
} }
export function createGovernanceTemplate(settings: ApiSettings, payload: Omit<GovernanceTemplateItem, "id" | "created_at" | "updated_at" | "effective_permission_count"> & { change_request_id?: string | null }): Promise<GovernanceTemplateItem> { 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> { export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string, changeRequestId?: string | null): Promise<void> {
const suffix = changeRequestId ? `?change_request_id=${encodeURIComponent(changeRequestId)}` : ""; return apiFetch(settings, apiPath(`/api/v1/admin/system/governance-templates/${templateId}`, { change_request_id: changeRequestId }), { method: "DELETE" });
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}${suffix}`, { method: "DELETE" });
} }
export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> { export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> {

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "@govoplan/core-webui";
import { fetchAdminOverview, type AdminOverview } from "../../api/admin"; import { fetchAdminOverview, type AdminOverview } from "../../api/admin";
import { Card } from "@govoplan/core-webui"; import { Card, MetricCard } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui";
import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui"; import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui";
@@ -43,7 +43,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
{hasAnySection(availableSections, platformSectionIds) && <Card title="ADMINISTRATION"> {hasAnySection(availableSections, platformSectionIds) && <Card title="ADMINISTRATION">
<div className="admin-overview-grid"> <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-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-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-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")} />} {availableSections.has("system-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />}
@@ -116,7 +116,7 @@ function hasAnySection(sections: ReadonlySet<string>, candidates: readonly strin
} }
function Metric({ title, value, text }: {title: string;value: string | number;text: string;}) { function Metric({ title, value, text }: {title: string;value: string | number;text: string;}) {
return <Card title={title}><strong className="module-big-number">{value}</strong><p className="muted">{text}</p></Card>; return <MetricCard label={title} value={value} detail={text} />;
} }
function AreaLink({ title, text, onClick }: {title: string;text: string;onClick: () => void;}) { function AreaLink({ title, text, onClick }: {title: string;text: string;onClick: () => void;}) {

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Check, RefreshCw } from "lucide-react"; import { Check, RefreshCw } from "lucide-react";
import type { ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks } from "@govoplan/core-webui"; import { AdminPageLayout, Button, Card, DataGrid, StatusBadge, TableActionGroup, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks, type DataGridColumn } from "@govoplan/core-webui";
import { import {
approveConfigurationChangeRequest, approveConfigurationChangeRequest,
fetchConfigurationChangesDelta, fetchConfigurationChangesDelta,
@@ -72,6 +72,47 @@ export default function ConfigurationChangesPanel({ settings, canApprove }: {set
} }
const pending = useMemo(() => requests.filter((item) => item.status !== "applied" && item.status !== "rejected"), [requests]); const pending = useMemo(() => requests.filter((item) => item.status !== "applied" && item.status !== "rejected"), [requests]);
const requestColumns: DataGridColumn<ConfigurationChangeRequest>[] = [
{
id: "setting",
header: "i18n:govoplan-admin.setting.fb449f71",
width: "minmax(220px, 1fr)",
minWidth: 180,
resizable: true,
sortable: true,
filterable: true,
value: (request) => `${request.label || request.key} ${request.key}`,
render: (request) => <div><strong>{request.label || request.key}</strong><span className="muted block">{request.key}</span></div>
},
{ id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 150, sortable: true, filterable: true, value: (request) => request.status, render: (request) => <StatusBadge status={statusTone(request.status)} label={request.status} /> },
{ id: "requested", header: "i18n:govoplan-admin.requested.c26bf60f", width: 180, sortable: true, value: (request) => request.requested_at, render: (request) => formatDateTime(request.requested_at) },
{ id: "approvals", header: "i18n:govoplan-admin.approvals.deb9d03c", width: 110, sortable: true, value: (request) => request.approvals.length },
{ id: "target", header: "i18n:govoplan-admin.target.61ad50a9", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, filterable: true, value: (request) => targetLabel(request.target), render: (request) => targetLabel(request.target) },
{
id: "actions",
header: "i18n:govoplan-admin.action.97c89a4d",
width: 72,
sticky: "end",
align: "right",
render: (request) => <TableActionGroup actions={[{
id: "approve",
label: "i18n:govoplan-admin.approve.7b2c7f14",
icon: <Check size={16} aria-hidden="true" />,
applicable: canApprove && request.status === "pending_approval",
disabled: busyId === request.id,
onClick: () => void approve(request)
}]} />
}
];
const historyColumns: DataGridColumn<ConfigurationChangeRecord>[] = [
{ id: "version", header: "i18n:govoplan-admin.version.2da600bf", width: 100, sortable: true, value: (record) => record.version, render: (record) => `#${record.version}` },
{ id: "setting", header: "i18n:govoplan-admin.setting.fb449f71", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (record) => `${record.key} ${record.id}`, render: (record) => <div><strong>{record.key}</strong><span className="muted block">{record.id}</span></div> },
{ id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 150, sortable: true, filterable: true, value: (record) => record.status, render: (record) => <StatusBadge status={statusTone(record.status)} label={record.status} /> },
{ id: "applied", header: "i18n:govoplan-admin.applied.a3e4a569", width: 180, sortable: true, value: (record) => record.created_at, render: (record) => formatDateTime(record.created_at) },
{ id: "approvers", header: "i18n:govoplan-admin.approvers.0e2de1fb", width: 120, sortable: true, value: (record) => record.approval_user_ids.length, render: (record) => record.approval_user_ids.length || "-" },
{ id: "target", header: "i18n:govoplan-admin.target.61ad50a9", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, filterable: true, value: (record) => targetLabel(record.target), render: (record) => targetLabel(record.target) }
];
return ( return (
<AdminPageLayout <AdminPageLayout
@@ -83,67 +124,23 @@ export default function ConfigurationChangesPanel({ settings, canApprove }: {set
actions={<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}> actions={<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}>
<Card title="i18n:govoplan-admin.requests.f7194e6a"> <Card title="i18n:govoplan-admin.requests.f7194e6a">
<div className="admin-table-wrap"> <DataGrid
<table className="admin-table"> id="admin-configuration-change-requests"
<thead> rows={pending}
<tr> columns={requestColumns}
<th>i18n:govoplan-admin.setting.fb449f71</th> getRowKey={(request) => request.id}
<th>i18n:govoplan-admin.status.bae7d5be</th> emptyText="i18n:govoplan-admin.no_open_requests.580c95f9"
<th>i18n:govoplan-admin.requested.c26bf60f</th> />
<th>i18n:govoplan-admin.approvals.deb9d03c</th>
<th>i18n:govoplan-admin.target.61ad50a9</th>
<th className="actions">i18n:govoplan-admin.action.97c89a4d</th>
</tr>
</thead>
<tbody>
{pending.map((request) =>
<tr key={request.id}>
<td><strong>{request.label || request.key}</strong><span className="muted block">{request.key}</span></td>
<td><StatusBadge status={statusTone(request.status)} label={request.status} /></td>
<td>{formatDateTime(request.requested_at)}</td>
<td>{request.approvals.length}</td>
<td>{targetLabel(request.target)}</td>
<td className="actions">
{canApprove && request.status === "pending_approval" &&
<Button onClick={() => void approve(request)} disabled={busyId === request.id}><Check size={16} /> i18n:govoplan-admin.approve.7b2c7f14</Button>
}
</td>
</tr>
)}
{!pending.length && <tr><td colSpan={6} className="muted">i18n:govoplan-admin.no_open_requests.580c95f9</td></tr>}
</tbody>
</table>
</div>
</Card> </Card>
<Card title="i18n:govoplan-admin.history.90ccd649"> <Card title="i18n:govoplan-admin.history.90ccd649">
<div className="admin-table-wrap"> <DataGrid
<table className="admin-table"> id="admin-configuration-change-history"
<thead> rows={history}
<tr> columns={historyColumns}
<th>i18n:govoplan-admin.version.2da600bf</th> getRowKey={(record) => record.id}
<th>i18n:govoplan-admin.setting.fb449f71</th> emptyText="i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7"
<th>i18n:govoplan-admin.status.bae7d5be</th> />
<th>i18n:govoplan-admin.applied.a3e4a569</th>
<th>i18n:govoplan-admin.approvers.0e2de1fb</th>
<th>i18n:govoplan-admin.target.61ad50a9</th>
</tr>
</thead>
<tbody>
{history.map((record) =>
<tr key={record.id}>
<td>#{record.version}</td>
<td><strong>{record.key}</strong><span className="muted block">{record.id}</span></td>
<td><StatusBadge status={statusTone(record.status)} label={record.status} /></td>
<td>{formatDateTime(record.created_at)}</td>
<td>{record.approval_user_ids.length || "-"}</td>
<td>{targetLabel(record.target)}</td>
</tr>
)}
{!history.length && <tr><td colSpan={6} className="muted">i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7</td></tr>}
</tbody>
</table>
</div>
</Card> </Card>
</AdminPageLayout>); </AdminPageLayout>);

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Check, Download, Play, RefreshCw } from "lucide-react"; import { Check, Download, Play, RefreshCw } from "lucide-react";
import type { ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, i18nMessage } from "@govoplan/core-webui"; import { AdminPageLayout, Button, Card, DataGrid, StatusBadge, adminErrorMessage, i18nMessage, type DataGridColumn } from "@govoplan/core-webui";
import { import {
applyConfigurationPackage, applyConfigurationPackage,
createConfigurationChangeRequest, createConfigurationChangeRequest,
@@ -273,89 +273,68 @@ function parseObject(text: string): {value: Record<string, unknown> | null;error
function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) { function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) {
if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>; if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>;
const columns: DataGridColumn<ConfigurationPackageDiagnostic>[] = [
{ id: "severity", header: "i18n:govoplan-admin.severity.de314fa0", width: 130, sortable: true, filterable: true, value: (item) => item.severity, render: (item) => <StatusBadge status={diagnosticTone(item.severity)} label={item.severity} /> },
{ id: "code", header: "i18n:govoplan-admin.code.adac6937", width: 190, sortable: true, filterable: true, value: (item) => item.code, render: (item) => <code>{item.code}</code> },
{ id: "owner", header: "i18n:govoplan-admin.owner.89ff3122", width: 150, sortable: true, filterable: true, value: (item) => item.module_id || "-", render: (item) => item.module_id || "-" },
{ id: "object", header: "i18n:govoplan-admin.object.2883f191", width: 180, sortable: true, filterable: true, value: (item) => item.object_ref || "-", render: (item) => item.object_ref || "-" },
{ id: "message", header: "i18n:govoplan-admin.message.68f4145f", width: "minmax(260px, 1fr)", minWidth: 220, resizable: true, filterable: true, value: (item) => `${item.message} ${item.resolution || ""}`, render: (item) => <div>{item.message}{item.resolution ? <span className="muted block">{item.resolution}</span> : null}</div> }
];
return ( return (
<div className="admin-table-wrap"> <DataGrid
<table className="admin-table"> id="admin-configuration-package-diagnostics"
<thead><tr><th>i18n:govoplan-admin.severity.de314fa0</th><th>i18n:govoplan-admin.code.adac6937</th><th>i18n:govoplan-admin.owner.89ff3122</th><th>i18n:govoplan-admin.object.2883f191</th><th>i18n:govoplan-admin.message.68f4145f</th></tr></thead> rows={diagnostics}
<tbody> columns={columns}
{diagnostics.map((item, index) => getRowKey={(item, index) => `${item.code}-${index}`}
<tr key={`${item.code}-${index}`}> />);
<td><StatusBadge status={diagnosticTone(item.severity)} label={item.severity} /></td>
<td><code>{item.code}</code></td>
<td>{item.module_id || "-"}</td>
<td>{item.object_ref || "-"}</td>
<td>{item.message}{item.resolution ? <span className="muted block">{item.resolution}</span> : null}</td>
</tr>
)}
</tbody>
</table>
</div>);
} }
function RequiredData({ items }: {items: ConfigurationPackageRequiredData[];}) { function RequiredData({ items }: {items: ConfigurationPackageRequiredData[];}) {
if (items.length === 0) return null; if (items.length === 0) return null;
const columns: DataGridColumn<ConfigurationPackageRequiredData>[] = [
{ id: "key", header: "i18n:govoplan-admin.key.c67dd20e", width: "minmax(200px, 1fr)", minWidth: 170, resizable: true, sortable: true, filterable: true, value: (item) => item.key, render: (item) => <code>{item.key}</code> },
{ id: "label", header: "i18n:govoplan-admin.label.74341e3c", width: "minmax(180px, 1fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (item) => item.label },
{ id: "type", header: "i18n:govoplan-admin.type.3deb7456", width: 140, sortable: true, filterable: true, value: (item) => item.data_type },
{ id: "required", header: "i18n:govoplan-admin.required.eed6bfb4", width: 110, sortable: true, value: (item) => item.required, render: (item) => item.required ? "yes" : "no" },
{ id: "secret", header: "i18n:govoplan-admin.secret.f4e7a874", width: 100, sortable: true, value: (item) => item.secret, render: (item) => item.secret ? "yes" : "no" }
];
return ( return (
<> <>
<h3>i18n:govoplan-admin.required_data.1b1c1b34</h3> <h3>i18n:govoplan-admin.required_data.1b1c1b34</h3>
<div className="admin-table-wrap"> <DataGrid id="admin-configuration-package-required-data" rows={items} columns={columns} getRowKey={(item) => item.key} />
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.key.c67dd20e</th><th>i18n:govoplan-admin.label.74341e3c</th><th>i18n:govoplan-admin.type.3deb7456</th><th>i18n:govoplan-admin.required.eed6bfb4</th><th>i18n:govoplan-admin.secret.f4e7a874</th></tr></thead>
<tbody>
{items.map((item) =>
<tr key={item.key}>
<td><code>{item.key}</code></td>
<td>{item.label}</td>
<td>{item.data_type}</td>
<td>{item.required ? "yes" : "no"}</td>
<td>{item.secret ? "yes" : "no"}</td>
</tr>
)}
</tbody>
</table>
</div>
</>); </>);
} }
function PackagePlan({ items }: {items: ConfigurationPackagePlanItem[];}) { function PackagePlan({ items }: {items: ConfigurationPackagePlanItem[];}) {
if (items.length === 0) return <p className="muted">i18n:govoplan-admin.no_plan_items.7108c582</p>; if (items.length === 0) return <p className="muted">i18n:govoplan-admin.no_plan_items.7108c582</p>;
const columns: DataGridColumn<ConfigurationPackagePlanItem>[] = [
{ id: "action", header: "i18n:govoplan-admin.action.97c89a4d", width: 130, sortable: true, filterable: true, value: (item) => item.action, render: (item) => <StatusBadge status={planTone(item.action)} label={item.action} /> },
{ id: "module", header: "i18n:govoplan-admin.module.b8ff0289", width: 170, sortable: true, filterable: true, value: (item) => item.module_id },
{ id: "fragment", header: "i18n:govoplan-admin.fragment.3f19d616", width: 170, sortable: true, filterable: true, value: (item) => item.fragment_type },
{ id: "id", header: "i18n:govoplan-admin.id.474ae526", width: 180, sortable: true, filterable: true, value: (item) => item.fragment_id || "-", render: (item) => item.fragment_id || "-" },
{ id: "summary", header: "i18n:govoplan-admin.summary.12b71c3e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.summary || "-", render: (item) => item.summary || "-" }
];
return ( return (
<> <>
<h3>i18n:govoplan-admin.plan.ae2f98a0</h3> <h3>i18n:govoplan-admin.plan.ae2f98a0</h3>
<div className="admin-table-wrap"> <DataGrid id="admin-configuration-package-plan" rows={items} columns={columns} getRowKey={(item, index) => `${item.module_id}-${item.fragment_type}-${item.fragment_id ?? index}`} />
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.action.97c89a4d</th><th>i18n:govoplan-admin.module.b8ff0289</th><th>i18n:govoplan-admin.fragment.3f19d616</th><th>i18n:govoplan-admin.id.474ae526</th><th>i18n:govoplan-admin.summary.12b71c3e</th></tr></thead>
<tbody>
{items.map((item, index) =>
<tr key={`${item.module_id}-${item.fragment_type}-${item.fragment_id ?? index}`}>
<td><StatusBadge status={planTone(item.action)} label={item.action} /></td>
<td>{item.module_id}</td>
<td>{item.fragment_type}</td>
<td>{item.fragment_id || "-"}</td>
<td>{item.summary || "-"}</td>
</tr>
)}
</tbody>
</table>
</div>
</>); </>);
} }
function ReferenceMap({ title, refs }: {title: string;refs: Record<string, string>;}) { function ReferenceMap({ title, refs }: {title: string;refs: Record<string, string>;}) {
const entries = Object.entries(refs); const entries = Object.entries(refs).map(([key, value]) => ({ key, value }));
if (entries.length === 0) return null; if (entries.length === 0) return null;
const columns: DataGridColumn<{key: string;value: string;}>[] = [
{ id: "key", header: "i18n:govoplan-admin.key.c67dd20e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (item) => item.key, render: (item) => <code>{item.key}</code> },
{ id: "value", header: "Value", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (item) => item.value }
];
return ( return (
<> <>
<h3>{title}</h3> <h3>{title}</h3>
<div className="admin-table-wrap"> <DataGrid id={`admin-configuration-package-refs-${title}`} rows={entries} columns={columns} getRowKey={(item) => item.key} />
<table className="admin-table">
<tbody>
{entries.map(([key, value]) => <tr key={key}><td><code>{key}</code></td><td>{value}</td></tr>)}
</tbody>
</table>
</div>
</>); </>);
} }

View File

@@ -19,7 +19,7 @@ import {
type PermissionItem, type PermissionItem,
type TenantAdminItem } from type TenantAdminItem } from
"../../api/admin"; "../../api/admin";
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
const emptyDraft = { const emptyDraft = {
slug: "", slug: "",
@@ -54,6 +54,7 @@ export default function GovernanceTemplatesPanel({
const [error, setError] = useState(""); const [error, setError] = useState("");
const [success, setSuccess] = useState(""); const [success, setSuccess] = useState("");
const dirty = editing !== null && draftKey(draft) !== savedDraftKey; const dirty = editing !== null && draftKey(draft) !== savedDraftKey;
const permissionsByScope = useMemo(() => new Map(permissions.map((permission) => [permission.scope, permission])), [permissions]);
useUnsavedDraftGuard({ useUnsavedDraftGuard({
dirty, dirty,
@@ -186,11 +187,11 @@ export default function GovernanceTemplatesPanel({
{ id: "status", header: "i18n:govoplan-admin.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: "status", header: "i18n:govoplan-admin.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-admin.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", id: "actions", header: "i18n:govoplan-admin.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right",
render: (row) => <div className="admin-icon-actions"> render: (row) => <TableActionGroup actions={[
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.inspect_value.9d5d1071", { value0: row.name })} icon={<Search />} onClick={() => setViewing(row)} /> { id: "inspect", label: i18nMessage("i18n:govoplan-admin.inspect_value.9d5d1071", { value0: row.name }), icon: <Search />, onClick: () => setViewing(row) },
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.edit_value.fad75899", { value0: row.name })} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canWrite} /> { id: "edit", label: i18nMessage("i18n:govoplan-admin.edit_value.fad75899", { value0: row.name }), icon: <Pencil />, disabled: !canWrite, onClick: () => openEdit(row) },
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.delete_value.4d18989e", { value0: row.name })} icon={<Trash2 />} variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} /> { id: "delete", label: i18nMessage("i18n:govoplan-admin.delete_value.4d18989e", { value0: row.name }), icon: <Trash2 />, variant: "danger", disabled: !canWrite, onClick: () => setDeleting(row) }
</div> ]} />
}], }],
[canWrite, kind, tenants]); [canWrite, kind, tenants]);
@@ -238,7 +239,7 @@ export default function GovernanceTemplatesPanel({
</Dialog> </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>}> <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> </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()} /> <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 { function draftKey(draft: typeof emptyDraft): string {
return JSON.stringify(draft); 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 }));
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, adminErrorMessage, Button, dispatchPlatformModulesChanged, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { AdminPageLayout, adminErrorMessage, Button, dispatchPlatformModulesChanged, formatDateTime, MetricCard, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard, type FormatDateTimeOptions } from "@govoplan/core-webui";
import { import {
cancelModuleInstallerRequest, cancelModuleInstallerRequest,
clearModuleInstallPlan, clearModuleInstallPlan,
@@ -323,11 +323,11 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{catalog && <> {catalog && <>
<div className="metric-grid module-management-metrics"> <div className="metric-grid module-management-metrics">
<ModuleMetric label="i18n:govoplan-admin.installed.7bb4405c" value={catalog.modules.length} detail="i18n:govoplan-admin.discovered_packages.660902de" /> <MetricCard label="i18n:govoplan-admin.installed.7bb4405c" value={catalog.modules.length} detail="i18n:govoplan-admin.discovered_packages.660902de" tone="info" />
<ModuleMetric label="i18n:govoplan-admin.active.a733b809" value={activeCount} detail="i18n:govoplan-admin.running_registry.5274b24b" /> <MetricCard label="i18n:govoplan-admin.active.a733b809" value={activeCount} detail="i18n:govoplan-admin.running_registry.5274b24b" tone="info" />
<ModuleMetric label="i18n:govoplan-admin.saved.c0ae8f6e" value={desiredCount} detail="i18n:govoplan-admin.startup_state.d1300be6" /> <MetricCard label="i18n:govoplan-admin.saved.c0ae8f6e" value={desiredCount} detail="i18n:govoplan-admin.startup_state.d1300be6" tone="info" />
<ModuleMetric label="i18n:govoplan-admin.drift.4876f7b9" value={pendingCount} detail={pendingCount ? "i18n:govoplan-admin.runtime_differs.43d1fd78" : "i18n:govoplan-admin.runtime_matches.a84afa48"} tone={pendingCount ? "warning" : "good"} /> <MetricCard label="i18n:govoplan-admin.drift.4876f7b9" value={pendingCount} detail={pendingCount ? "i18n:govoplan-admin.runtime_differs.43d1fd78" : "i18n:govoplan-admin.runtime_matches.a84afa48"} tone={pendingCount ? "warning" : "good"} />
<ModuleMetric label="i18n:govoplan-admin.maintenance.94de303b" value={maintenanceEnabled ? "i18n:govoplan-admin.on.e0049a66" : "i18n:govoplan-admin.off.e3de5ab0"} detail={canAccessMaintenance ? "i18n:govoplan-admin.bypass_allowed.4e347c27" : "i18n:govoplan-admin.bypass_denied.ab987400"} tone={maintenanceEnabled ? "warning" : "info"} /> <MetricCard label="i18n:govoplan-admin.maintenance.94de303b" value={maintenanceEnabled ? "i18n:govoplan-admin.on.e0049a66" : "i18n:govoplan-admin.off.e3de5ab0"} detail={canAccessMaintenance ? "i18n:govoplan-admin.bypass_allowed.4e347c27" : "i18n:govoplan-admin.bypass_denied.ab987400"} tone={maintenanceEnabled ? "warning" : "info"} />
</div> </div>
{dirty && <div className="module-management-notes"> {dirty && <div className="module-management-notes">
@@ -355,8 +355,8 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
<div className="module-management-meta"> <div className="module-management-meta">
<ModuleStatus module={module} desiredEnabled={desiredEnabled} /> <ModuleStatus module={module} desiredEnabled={desiredEnabled} />
{module.protected && <StatusBadge status="locked" label="i18n:govoplan-admin.locked.a798882f" />} {module.protected && <StatusBadge status="locked" label="i18n:govoplan-admin.locked.a798882f" />}
{module.frontend_package && <span>{module.frontend_package}</span>} <span>i18n:govoplan-admin.webui_package.19645536 <code>{module.frontend_package ?? "none"}</code></span>
{module.migration_module_id && <span>i18n:govoplan-admin.db.e355c23a {module.migration_module_id}</span>} <span>i18n:govoplan-admin.db.e355c23a <code>{module.migration_module_id ?? "none"}</code></span>
</div> </div>
<div className="module-management-details"> <div className="module-management-details">
<span>i18n:govoplan-admin.requires.a4fc9357 {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span> <span>i18n:govoplan-admin.requires.a4fc9357 {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span>
@@ -596,10 +596,10 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
<StatusBadge status={statusTone(request.status)} label={request.status} /> <StatusBadge status={statusTone(request.status)} label={request.status} />
</div> </div>
<div className="module-management-details"> <div className="module-management-details">
<span>i18n:govoplan-admin.created.0c78dab1 {formatDateTime(request.created_at)}</span> <span>i18n:govoplan-admin.created.0c78dab1 {formatDateTime(request.created_at, ADMIN_DATE_TIME_OPTIONS)}</span>
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(request.started_at)}</span> <span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(request.started_at, ADMIN_DATE_TIME_OPTIONS)}</span>
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(request.finished_at)}</span> <span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(request.finished_at, ADMIN_DATE_TIME_OPTIONS)}</span>
{request.cancelled_at && <span>i18n:govoplan-admin.cancelled.8c9dcfa8 {formatDateTime(request.cancelled_at)}</span>} {request.cancelled_at && <span>i18n:govoplan-admin.cancelled.8c9dcfa8 {formatDateTime(request.cancelled_at, ADMIN_DATE_TIME_OPTIONS)}</span>}
{request.retry_of && <span>i18n:govoplan-admin.retry_of.3a6bb304 {request.retry_of}</span>} {request.retry_of && <span>i18n:govoplan-admin.retry_of.3a6bb304 {request.retry_of}</span>}
{traceId(request.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(request.trace)}</code></span>} {traceId(request.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(request.trace)}</code></span>}
</div> </div>
@@ -639,8 +639,8 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{run.request_id && <span>i18n:govoplan-admin.request.43c2e7a7 {run.request_id}</span>} {run.request_id && <span>i18n:govoplan-admin.request.43c2e7a7 {run.request_id}</span>}
{traceId(run.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(run.trace)}</code></span>} {traceId(run.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(run.trace)}</code></span>}
<span>i18n:govoplan-admin.commands.f6a34aed {run.commands_count}</span> <span>i18n:govoplan-admin.commands.f6a34aed {run.commands_count}</span>
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(run.started_at)}</span> <span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(run.started_at, ADMIN_DATE_TIME_OPTIONS)}</span>
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(run.finished_at)}</span> <span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(run.finished_at, ADMIN_DATE_TIME_OPTIONS)}</span>
</div> </div>
{run.error && <p className="module-installer-run-error">{run.error}</p>} {run.error && <p className="module-installer-run-error">{run.error}</p>}
</div> </div>
@@ -654,10 +654,6 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
} }
function ModuleMetric({ label, value, detail, tone = "info" }: {label: string;value: string | number;detail: string;tone?: "info" | "good" | "warning";}) {
return <div className={`metric-card metric-${tone}`}><div className="metric-label">{label}</div><div className="metric-value">{value}</div><div className="metric-detail">{detail}</div></div>;
}
function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) { function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) {
const label = license.license_id ? `${license.license_id}${license.subject ? ` · ${license.subject}` : ""}` : license.configured ? "i18n:govoplan-admin.configured_license.3e73f8f5" : "i18n:govoplan-admin.no_license_configured.693cea1a"; const label = license.license_id ? `${license.license_id}${license.subject ? ` · ${license.subject}` : ""}` : license.configured ? "i18n:govoplan-admin.configured_license.3e73f8f5" : "i18n:govoplan-admin.no_license_configured.693cea1a";
return ( return (
@@ -669,7 +665,7 @@ function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) {
</div> </div>
<p className="module-package-catalog-description">{label}</p> <p className="module-package-catalog-description">{label}</p>
<div className="module-management-details"> <div className="module-management-details">
<span>i18n:govoplan-admin.valid.b374b8f9 {formatDateTime(license.valid_from)} to {formatDateTime(license.valid_until)}</span> <span>i18n:govoplan-admin.valid.b374b8f9 {formatDateTime(license.valid_from, ADMIN_DATE_TIME_OPTIONS)} to {formatDateTime(license.valid_until, ADMIN_DATE_TIME_OPTIONS)}</span>
<span>i18n:govoplan-admin.features.5df81ffa {license.features.length ? license.features.join(", ") : "none"}</span> <span>i18n:govoplan-admin.features.5df81ffa {license.features.length ? license.features.join(", ") : "none"}</span>
{license.required_features.length > 0 && <span>i18n:govoplan-admin.required.d5793988 {license.required_features.join(", ")}</span>} {license.required_features.length > 0 && <span>i18n:govoplan-admin.required.d5793988 {license.required_features.join(", ")}</span>}
{license.missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {license.missing_features.join(", ")}</span>} {license.missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {license.missing_features.join(", ")}</span>}
@@ -851,12 +847,16 @@ function licenseLabel(license: ModuleLicenseDiagnostics): string {
return "i18n:govoplan-admin.unsigned.e91344ea"; return "i18n:govoplan-admin.unsigned.e91344ea";
} }
function formatDateTime(value?: string | null): string { const ADMIN_DATE_TIME_OPTIONS: FormatDateTimeOptions = {
if (!value) return "i18n:govoplan-admin.not_recorded.9925ee3c"; fallback: "i18n:govoplan-admin.not_recorded.9925ee3c",
const date = new Date(value); year: "numeric",
if (Number.isNaN(date.getTime())) return value; month: "numeric",
return date.toLocaleString(); day: "numeric",
} hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: undefined
};
function traceId(value?: Record<string, unknown> | null): string { function traceId(value?: Record<string, unknown> | null): string {
const correlationId = value?.correlation_id; const correlationId = value?.correlation_id;

View File

@@ -5,7 +5,7 @@ import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui";
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin"; import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin";
import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { AdminPageLayout, AdminSelectionList, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
const DELTA_KEY = "admin:system-settings"; const DELTA_KEY = "admin:system-settings";
@@ -112,10 +112,8 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
{setBusy(false);} {setBusy(false);}
} }
function toggleLanguage(code: string, checked: boolean) { function setEnabledLanguages(selected: string[]) {
const enabled = new Set(draft.enabled_language_codes); const enabled = new Set(selected);
if (checked) enabled.add(code);
else enabled.delete(code);
const nextEnabled = draft.available_languages.map((item) => item.code).filter((item) => enabled.has(item)); const nextEnabled = draft.available_languages.map((item) => item.code).filter((item) => enabled.has(item));
const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale); const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale);
setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale }); setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale });
@@ -159,18 +157,11 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
</FormField> </FormField>
</Card> </Card>
<Card title="i18n:govoplan-admin.language_packages"> <Card title="i18n:govoplan-admin.language_packages">
<div className="settings-list"> <AdminSelectionList
{draft.available_languages.map((item) => ( options={draft.available_languages.map((item) => ({ id: item.code, label: item.code.toUpperCase(), description: languageOptionLabel(item), disabled: !canWrite || busy || item.code === draft.default_locale }))}
<label className="admin-inline-check" key={item.code}> selected={draft.enabled_language_codes}
<input onChange={setEnabledLanguages}
type="checkbox" />
checked={draft.enabled_language_codes.includes(item.code)}
disabled={!canWrite || busy || item.code === draft.default_locale}
onChange={(event) => toggleLanguage(item.code, event.target.checked)} />
<span><strong>{item.code.toUpperCase()}</strong> {languageOptionLabel(item)}</span>
</label>
))}
</div>
<div className="form-grid"> <div className="form-grid">
<FormField label="i18n:govoplan-admin.language_code"> <FormField label="i18n:govoplan-admin.language_code">
<input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} /> <input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} />

View File

@@ -14,7 +14,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-admin.after.695c89be": "After:", "i18n:govoplan-admin.after.695c89be": "After:",
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template", "i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item", "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.admin.4e7afebc": "Admin",
"i18n:govoplan-admin.administration.b8be3d12": "Administration", "i18n:govoplan-admin.administration.b8be3d12": "Administration",
"i18n:govoplan-admin.platform_administration": "Platform 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_group_template.72407248": "Create group template",
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply", "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_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.0c78dab1": "Created:",
"i18n:govoplan-admin.created.accf40c8": "Created", "i18n:govoplan-admin.created.accf40c8": "Created",
"i18n:govoplan-admin.current_window.73e25c9f": "Current window:", "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.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_group_template.8745d842": "Delete group template",
"i18n:govoplan-admin.delete_template.399bf72a": "Delete 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_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.delete_value.4d18989e": "Delete {value0}",
"i18n:govoplan-admin.dependencies.9f4f78d1": "Dependencies:", "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_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.dry_run.3d14659c": "Dry-run",
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template", "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.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode", "i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
"i18n:govoplan-admin.enabled_modules.3c38e9ff": "Enabled modules:", "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_plan.842dc280": "Save plan",
"i18n:govoplan-admin.save_settings.913aba9f": "Save settings", "i18n:govoplan-admin.save_settings.913aba9f": "Save settings",
"i18n:govoplan-admin.save_template.0885fab2": "Save template", "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.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.saved.c0ae8f6e": "Saved",
"i18n:govoplan-admin.saving.56a2285c": "Saving…", "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_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_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_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_permissions.246294bc": "Permissions",
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role", "i18n:govoplan-admin.tenant_role.6b53115d": "Role template",
"i18n:govoplan-admin.tenant_roles.51aca82d": "Tenant roles", "i18n:govoplan-admin.tenant_roles.51aca82d": "Role templates",
"i18n:govoplan-admin.tenant_users.cb800b38": "Tenant users", "i18n:govoplan-admin.tenant_users.cb800b38": "Tenant users",
"i18n:govoplan-admin.tenants.1f7ae776": "Tenants", "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.", "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.after.695c89be": "Nach:",
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template", "i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item", "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.admin.4e7afebc": "Administration",
"i18n:govoplan-admin.administration.b8be3d12": "Administration", "i18n:govoplan-admin.administration.b8be3d12": "Administration",
"i18n:govoplan-admin.platform_administration": "Plattformadministration", "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_group_template.72407248": "Create group template",
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply", "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_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.0c78dab1": "Created:",
"i18n:govoplan-admin.created.accf40c8": "Erstellt", "i18n:govoplan-admin.created.accf40c8": "Erstellt",
"i18n:govoplan-admin.current_window.73e25c9f": "Aktuelles Fenster:", "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.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_group_template.8745d842": "Delete group template",
"i18n:govoplan-admin.delete_template.399bf72a": "Delete 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_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.delete_value.4d18989e": "Delete {value0}",
"i18n:govoplan-admin.dependencies.9f4f78d1": "Abhaengigkeiten:", "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_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.dry_run.3d14659c": "Dry-run",
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template", "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.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode", "i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
"i18n:govoplan-admin.enabled_modules.3c38e9ff": "Aktive Module:", "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_plan.842dc280": "Save plan",
"i18n:govoplan-admin.save_settings.913aba9f": "Einstellungen speichern", "i18n:govoplan-admin.save_settings.913aba9f": "Einstellungen speichern",
"i18n:govoplan-admin.save_template.0885fab2": "Save template", "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.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.saved.c0ae8f6e": "Gespeichert",
"i18n:govoplan-admin.saving.56a2285c": "Saving…", "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_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_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_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_permissions.246294bc": "Berechtigungen",
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role", "i18n:govoplan-admin.tenant_role.6b53115d": "Rollenvorlage",
"i18n:govoplan-admin.tenant_roles.51aca82d": "Mandantenrollen", "i18n:govoplan-admin.tenant_roles.51aca82d": "Rollenvorlagen",
"i18n:govoplan-admin.tenant_users.cb800b38": "Mandantenbenutzer", "i18n:govoplan-admin.tenant_users.cb800b38": "Mandantenbenutzer",
"i18n:govoplan-admin.tenants.1f7ae776": "Mandanten", "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.", "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.",

View File

@@ -19,6 +19,7 @@ const adminSections: AdminSectionsUiCapability = {
sections: [ sections: [
{ {
id: "overview", id: "overview",
surfaceId: "admin.section.overview",
label: "i18n:govoplan-admin.overview.0efc2e6b", label: "i18n:govoplan-admin.overview.0efc2e6b",
group: "ROOT", group: "ROOT",
order: 0, order: 0,
@@ -31,6 +32,7 @@ const adminSections: AdminSectionsUiCapability = {
}, },
{ {
id: "system-settings", id: "system-settings",
surfaceId: "admin.section.system-settings",
label: "i18n:govoplan-admin.general.9239ee2c", label: "i18n:govoplan-admin.general.9239ee2c",
group: "SYSTEM", group: "SYSTEM",
order: 10, order: 10,
@@ -43,6 +45,7 @@ const adminSections: AdminSectionsUiCapability = {
}, },
{ {
id: "system-configuration-changes", id: "system-configuration-changes",
surfaceId: "admin.section.system-configuration-changes",
label: "i18n:govoplan-admin.changes.8aa57de6", label: "i18n:govoplan-admin.changes.8aa57de6",
group: "SYSTEM", group: "SYSTEM",
order: 20, order: 20,
@@ -54,7 +57,8 @@ const adminSections: AdminSectionsUiCapability = {
}, },
{ {
id: "system-configuration-packages", id: "system-configuration-packages",
label: "i18n:govoplan-admin.packages.0a999012", surfaceId: "admin.section.system-configuration-packages",
label: "i18n:govoplan-admin.configuration_packages.eb2f05f1",
group: "SYSTEM", group: "SYSTEM",
order: 30, order: 30,
allOf: ["system:settings:read"], allOf: ["system:settings:read"],
@@ -65,6 +69,7 @@ const adminSections: AdminSectionsUiCapability = {
}, },
{ {
id: "system-role-templates", id: "system-role-templates",
surfaceId: "admin.section.system-role-templates",
label: "i18n:govoplan-admin.tenant_roles.51aca82d", label: "i18n:govoplan-admin.tenant_roles.51aca82d",
group: "SYSTEM", group: "SYSTEM",
order: 40, order: 40,
@@ -78,6 +83,7 @@ const adminSections: AdminSectionsUiCapability = {
}, },
{ {
id: "system-modules", id: "system-modules",
surfaceId: "admin.section.system-modules",
label: "i18n:govoplan-admin.modules.04e9462c", label: "i18n:govoplan-admin.modules.04e9462c",
group: "SYSTEM", group: "SYSTEM",
order: 85, order: 85,
@@ -90,6 +96,7 @@ const adminSections: AdminSectionsUiCapability = {
}, },
{ {
id: "system-groups", id: "system-groups",
surfaceId: "admin.section.system-groups",
label: "i18n:govoplan-admin.groups.ae9629f4", label: "i18n:govoplan-admin.groups.ae9629f4",
group: "SYSTEM", group: "SYSTEM",
order: 50, order: 50,
@@ -110,6 +117,15 @@ export const adminModule: PlatformWebModule = {
version: "1.0.0", version: "1.0.0",
dependencies: ["access"], dependencies: ["access"],
translations, translations,
viewSurfaces: [
{ id: "admin.section.overview", moduleId: "admin", kind: "section", label: "Administration overview", order: 0 },
{ id: "admin.section.system-settings", moduleId: "admin", kind: "section", label: "System settings", order: 10 },
{ id: "admin.section.system-configuration-changes", moduleId: "admin", kind: "section", label: "Configuration changes", order: 20 },
{ id: "admin.section.system-configuration-packages", moduleId: "admin", kind: "section", label: "Configuration packages", order: 30 },
{ id: "admin.section.system-role-templates", moduleId: "admin", kind: "section", label: "Role templates", order: 40 },
{ id: "admin.section.system-groups", moduleId: "admin", kind: "section", label: "Group templates", order: 50 },
{ id: "admin.section.system-modules", moduleId: "admin", kind: "section", label: "Modules", order: 85 }
],
uiCapabilities: { uiCapabilities: {
"admin.sections": adminSections "admin.sections": adminSections
} }