chore: sync GovOPlaN module split state
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
webui/node_modules/
|
||||||
|
webui/dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.component-test-build/
|
||||||
|
.module-test-build/
|
||||||
|
.policy-test-build/
|
||||||
|
.template-preview-test-build/
|
||||||
|
.import-test-build/
|
||||||
|
webui/.component-test-build/
|
||||||
|
webui/.module-test-build/
|
||||||
|
webui/.policy-test-build/
|
||||||
|
webui/.template-preview-test-build/
|
||||||
|
webui/.import-test-build/
|
||||||
26
README.md
26
README.md
@@ -3,9 +3,10 @@
|
|||||||
`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.
|
||||||
|
|
||||||
This repository owns the live `governance_templates` and
|
This repository owns the live `admin_governance_templates` and
|
||||||
`governance_template_assignments` tables while preserving their historical
|
`admin_governance_template_assignments` tables. Core migrations rename the
|
||||||
table names. It contributes the stable
|
historical unprefixed governance tables for existing development databases. It
|
||||||
|
contributes the stable
|
||||||
`/api/v1/admin/system/governance-templates` routes and owns governance-template
|
`/api/v1/admin/system/governance-templates` routes and owns governance-template
|
||||||
CRUD plus materialization into access-owned tenant groups and roles.
|
CRUD plus materialization into access-owned tenant groups and roles.
|
||||||
|
|
||||||
@@ -23,3 +24,22 @@ section entries through core's `admin.sections` UI capability:
|
|||||||
|
|
||||||
The route shell in access collects those sections at runtime, applies their
|
The route shell in access collects those sections at runtime, applies their
|
||||||
scope requirements, and renders them without importing admin package internals.
|
scope requirements, and renders them without importing admin package internals.
|
||||||
|
|
||||||
|
## Module Lifecycle Administration
|
||||||
|
|
||||||
|
The admin module owns the operator surfaces for module lifecycle management:
|
||||||
|
|
||||||
|
- installed/enabled/desired module state
|
||||||
|
- runtime activation and deactivation of installed modules
|
||||||
|
- signed catalog install planning
|
||||||
|
- non-destructive uninstall planning, with explicit `destroy_data` retirement
|
||||||
|
options where a module provides a retirement provider
|
||||||
|
- installer preflight status, maintenance-mode blockers, migration/restart
|
||||||
|
checklist entries, and rendered operator commands
|
||||||
|
- installer daemon request queue, cancellation/retry, recent run summaries,
|
||||||
|
rollback status, run IDs, request IDs, and trace IDs
|
||||||
|
|
||||||
|
Package mutation is intentionally not executed inside the FastAPI request. The
|
||||||
|
admin UI records operator intent and queues or renders commands for the trusted
|
||||||
|
installer process described in
|
||||||
|
`/mnt/DATA/git/govoplan-core/docs/MODULE_ARCHITECTURE.md`.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.6",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
Metadata-Version: 2.4
|
|
||||||
Name: govoplan-admin
|
|
||||||
Version: 0.1.4
|
|
||||||
Summary: GovOPlaN generic administration module.
|
|
||||||
Author: GovOPlaN
|
|
||||||
Requires-Python: >=3.12
|
|
||||||
Description-Content-Type: text/markdown
|
|
||||||
Requires-Dist: govoplan-core>=0.1.4
|
|
||||||
Requires-Dist: govoplan-access>=0.1.4
|
|
||||||
|
|
||||||
# GovOPlaN Admin
|
|
||||||
|
|
||||||
`govoplan-admin` owns generic system administration API and WebUI contributions
|
|
||||||
during the GovOPlaN module split.
|
|
||||||
|
|
||||||
This repository owns the live `governance_templates` and
|
|
||||||
`governance_template_assignments` tables while preserving their historical
|
|
||||||
table names. It contributes the stable
|
|
||||||
`/api/v1/admin/system/governance-templates` routes and owns governance-template
|
|
||||||
CRUD plus materialization into access-owned tenant groups and roles.
|
|
||||||
|
|
||||||
## WebUI Package
|
|
||||||
|
|
||||||
The repository root and `webui/` directory both expose the package
|
|
||||||
`@govoplan/admin-webui`. The package does not contribute the `/admin` route
|
|
||||||
itself; `govoplan-access` owns the route shell. Instead, admin contributes
|
|
||||||
section entries through core's `admin.sections` UI capability:
|
|
||||||
|
|
||||||
- overview
|
|
||||||
- system settings
|
|
||||||
- governance template tenant roles
|
|
||||||
- governance template groups
|
|
||||||
|
|
||||||
The route shell in access collects those sections at runtime, applies their
|
|
||||||
scope requirements, and renders them without importing admin package internals.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
README.md
|
|
||||||
pyproject.toml
|
|
||||||
src/govoplan_admin/__init__.py
|
|
||||||
src/govoplan_admin/py.typed
|
|
||||||
src/govoplan_admin.egg-info/PKG-INFO
|
|
||||||
src/govoplan_admin.egg-info/SOURCES.txt
|
|
||||||
src/govoplan_admin.egg-info/dependency_links.txt
|
|
||||||
src/govoplan_admin.egg-info/entry_points.txt
|
|
||||||
src/govoplan_admin.egg-info/requires.txt
|
|
||||||
src/govoplan_admin.egg-info/top_level.txt
|
|
||||||
src/govoplan_admin/backend/__init__.py
|
|
||||||
src/govoplan_admin/backend/governance.py
|
|
||||||
src/govoplan_admin/backend/manifest.py
|
|
||||||
src/govoplan_admin/backend/api/__init__.py
|
|
||||||
src/govoplan_admin/backend/api/v1/__init__.py
|
|
||||||
src/govoplan_admin/backend/api/v1/routes.py
|
|
||||||
src/govoplan_admin/backend/api/v1/schemas.py
|
|
||||||
src/govoplan_admin/backend/db/__init__.py
|
|
||||||
src/govoplan_admin/backend/db/models.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[govoplan.modules]
|
|
||||||
admin = govoplan_admin.backend.manifest:get_manifest
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
govoplan-core>=0.1.4
|
|
||||||
govoplan-access>=0.1.4
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
govoplan_admin
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,17 +2,33 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from govoplan_admin.backend.governance import create_template, delete_template, update_template
|
from govoplan_admin.backend.governance import create_template, delete_template, update_template
|
||||||
from govoplan_core.admin.settings import get_system_settings
|
from govoplan_core.admin.settings import get_system_settings
|
||||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope, require_any_scope, require_scope
|
from govoplan_access.auth import ApiPrincipal, has_scope, require_any_scope, require_scope
|
||||||
from govoplan_core.audit.logging import audit_from_principal
|
from govoplan_core.audit.logging import audit_from_principal, audit_operation_context
|
||||||
from govoplan_admin.backend.db.models import GovernanceTemplate, GovernanceTemplateAssignment
|
from govoplan_admin.backend.db.models import GovernanceTemplate, GovernanceTemplateAssignment
|
||||||
from govoplan_core.admin.common import AdminConflictError, AdminValidationError
|
from govoplan_core.admin.common import AdminConflictError, AdminValidationError
|
||||||
from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration
|
from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration
|
||||||
|
from govoplan_core.core.change_sequence import (
|
||||||
|
decode_sequence_watermark,
|
||||||
|
encode_sequence_watermark,
|
||||||
|
max_sequence_id,
|
||||||
|
record_change,
|
||||||
|
sequence_entries_since,
|
||||||
|
sequence_watermark_is_expired,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.pagination import KeysetCursorError, decode_keyset_cursor, encode_keyset_cursor, keyset_query_fingerprint
|
||||||
|
from govoplan_core.core.configuration_control import (
|
||||||
|
ConfigurationChangeApproval,
|
||||||
|
ConfigurationControlError,
|
||||||
|
ensure_configuration_change_allowed,
|
||||||
|
record_configuration_change_applied,
|
||||||
|
)
|
||||||
from govoplan_core.core.module_management import (
|
from govoplan_core.core.module_management import (
|
||||||
PROTECTED_MODULES,
|
PROTECTED_MODULES,
|
||||||
ModuleInstallPlanItem,
|
ModuleInstallPlanItem,
|
||||||
@@ -41,13 +57,20 @@ 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_license import module_license_decision
|
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
|
||||||
from govoplan_core.core.lifecycle import ModuleLifecycleManager
|
from govoplan_core.core.lifecycle import ModuleLifecycleManager
|
||||||
from govoplan_core.core.registry import PlatformRegistry
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_core.core.runtime import get_registry
|
from govoplan_core.core.runtime import get_registry
|
||||||
from govoplan_core.db.session import get_session
|
from govoplan_core.db.session import get_session
|
||||||
|
from govoplan_core.i18n import (
|
||||||
|
i18n_settings,
|
||||||
|
normalize_enabled_language_codes,
|
||||||
|
normalize_language_packages,
|
||||||
|
system_i18n_payload,
|
||||||
|
update_i18n_settings,
|
||||||
|
)
|
||||||
from govoplan_core.privacy.retention import privacy_policy_from_settings, set_privacy_policy
|
from govoplan_core.privacy.retention import privacy_policy_from_settings, set_privacy_policy
|
||||||
from govoplan_core.security.permissions import ALL_PERMISSIONS, effective_permission_count
|
from govoplan_core.security.permissions import ALL_PERMISSIONS, effective_permission_count
|
||||||
from govoplan_core.server.registry import available_module_manifests
|
from govoplan_core.server.registry import available_module_manifests
|
||||||
@@ -59,6 +82,7 @@ from .schemas import (
|
|||||||
AdminOverviewResponse,
|
AdminOverviewResponse,
|
||||||
GovernanceTemplateCreateRequest,
|
GovernanceTemplateCreateRequest,
|
||||||
GovernanceTemplateItem,
|
GovernanceTemplateItem,
|
||||||
|
GovernanceTemplateListDeltaResponse,
|
||||||
GovernanceTemplateListResponse,
|
GovernanceTemplateListResponse,
|
||||||
GovernanceTemplateUpdateRequest,
|
GovernanceTemplateUpdateRequest,
|
||||||
MaintenanceModeItem,
|
MaintenanceModeItem,
|
||||||
@@ -76,14 +100,33 @@ from .schemas import (
|
|||||||
ModuleInstallPlanUpdateRequest,
|
ModuleInstallPlanUpdateRequest,
|
||||||
ModulePackageCatalogItem,
|
ModulePackageCatalogItem,
|
||||||
ModulePackageCatalogResponse,
|
ModulePackageCatalogResponse,
|
||||||
|
ModuleLicenseDiagnostics,
|
||||||
ModuleStateUpdateRequest,
|
ModuleStateUpdateRequest,
|
||||||
PrivacyRetentionPolicyItem,
|
PrivacyRetentionPolicyItem,
|
||||||
|
SystemSettingsDeltaResponse,
|
||||||
SystemSettingsItem,
|
SystemSettingsItem,
|
||||||
SystemSettingsUpdateRequest,
|
SystemSettingsUpdateRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
|
|
||||||
|
ADMIN_MODULE_ID = "admin"
|
||||||
|
SYSTEM_SETTINGS_COLLECTION = "admin.system_settings"
|
||||||
|
SYSTEM_SETTINGS_RESOURCE = "system_settings_section"
|
||||||
|
GOVERNANCE_TEMPLATES_COLLECTION = "admin.governance_templates"
|
||||||
|
GOVERNANCE_TEMPLATE_RESOURCE = "governance_template"
|
||||||
|
INSTALLER_RUNS_CURSOR_SCOPE = "admin.installer.runs.v1"
|
||||||
|
INSTALLER_REQUESTS_CURSOR_SCOPE = "admin.installer.requests.v1"
|
||||||
|
DEFAULT_INSTALLER_HISTORY_PAGE_SIZE = 25
|
||||||
|
SYSTEM_SETTINGS_SECTIONS = (
|
||||||
|
"defaults",
|
||||||
|
"tenant_capabilities",
|
||||||
|
"languages",
|
||||||
|
"privacy_retention_policy",
|
||||||
|
"maintenance_mode",
|
||||||
|
"settings",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _access_administration() -> AccessAdministration:
|
def _access_administration() -> AccessAdministration:
|
||||||
registry = get_registry()
|
registry = get_registry()
|
||||||
@@ -113,10 +156,136 @@ 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))
|
||||||
if isinstance(exc, AdminValidationError):
|
if isinstance(exc, AdminValidationError):
|
||||||
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
|
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc))
|
||||||
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
def _configuration_control_http_error(exc: ConfigurationControlError) -> HTTPException:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail={"code": exc.code, "message": str(exc), "plan": exc.plan},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_delta_watermark(session: Session, collections: tuple[str, ...]) -> str:
|
||||||
|
return encode_sequence_watermark(max_sequence_id(session, module_id=ADMIN_MODULE_ID, collections=collections))
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_delta_entries(session: Session, *, collections: tuple[str, ...], since: str, limit: int):
|
||||||
|
try:
|
||||||
|
since_sequence = decode_sequence_watermark(since)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
if sequence_watermark_is_expired(
|
||||||
|
session,
|
||||||
|
since=since_sequence,
|
||||||
|
module_id=ADMIN_MODULE_ID,
|
||||||
|
collections=collections,
|
||||||
|
):
|
||||||
|
return None, False
|
||||||
|
entries_plus_one = sequence_entries_since(
|
||||||
|
session,
|
||||||
|
since=since_sequence,
|
||||||
|
module_id=ADMIN_MODULE_ID,
|
||||||
|
collections=collections,
|
||||||
|
limit=limit + 1,
|
||||||
|
)
|
||||||
|
has_more = len(entries_plus_one) > limit
|
||||||
|
return entries_plus_one[:limit], has_more
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_delta_response_watermark(session: Session, *, collections: tuple[str, ...], entries, has_more: bool) -> str:
|
||||||
|
return encode_sequence_watermark(entries[-1].id) if has_more and entries else _admin_delta_watermark(session, collections)
|
||||||
|
|
||||||
|
|
||||||
|
def _history_cursor_fingerprint(scope: str, *, page_size: int) -> str:
|
||||||
|
return keyset_query_fingerprint(scope, {"page_size": page_size, "sort": "id.desc"})
|
||||||
|
|
||||||
|
|
||||||
|
def _window_history_records(records: list[dict[str, object]], *, id_key: str, scope: str, page_size: int, cursor: str | None):
|
||||||
|
start = 0
|
||||||
|
full = False
|
||||||
|
fingerprint = _history_cursor_fingerprint(scope, page_size=page_size)
|
||||||
|
if cursor:
|
||||||
|
try:
|
||||||
|
values = decode_keyset_cursor(scope, cursor, fingerprint=fingerprint)
|
||||||
|
except KeysetCursorError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
after_id = values.get(id_key) if values else None
|
||||||
|
if not isinstance(after_id, str):
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid pagination cursor")
|
||||||
|
matching_index = next((index for index, item in enumerate(records) if str(item.get(id_key) or "") == after_id), None)
|
||||||
|
if matching_index is None:
|
||||||
|
full = True
|
||||||
|
else:
|
||||||
|
start = matching_index + 1
|
||||||
|
page = records[start:start + page_size]
|
||||||
|
next_cursor = None
|
||||||
|
if start + page_size < len(records) and page:
|
||||||
|
next_cursor = encode_keyset_cursor(scope, fingerprint=fingerprint, values={id_key: str(page[-1].get(id_key) or ""), "page_size": page_size})
|
||||||
|
return page, next_cursor, full
|
||||||
|
|
||||||
|
|
||||||
|
def _system_settings_item(session: Session) -> SystemSettingsItem:
|
||||||
|
item = get_system_settings(session)
|
||||||
|
policy = privacy_policy_from_settings(item)
|
||||||
|
maintenance_mode = saved_maintenance_mode(session)
|
||||||
|
i18n_payload = system_i18n_payload(item)
|
||||||
|
return SystemSettingsItem(
|
||||||
|
default_locale=item.default_locale,
|
||||||
|
allow_tenant_custom_groups=item.allow_tenant_custom_groups,
|
||||||
|
allow_tenant_custom_roles=item.allow_tenant_custom_roles,
|
||||||
|
allow_tenant_api_keys=item.allow_tenant_api_keys,
|
||||||
|
privacy_retention_policy=PrivacyRetentionPolicyItem.model_validate(policy.model_dump(mode="json")),
|
||||||
|
maintenance_mode=MaintenanceModeItem.model_validate(maintenance_mode.as_dict()),
|
||||||
|
available_languages=i18n_payload["available_languages"],
|
||||||
|
enabled_language_codes=i18n_payload["enabled_languages"],
|
||||||
|
settings=item.settings or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _system_settings_sections(item: SystemSettingsItem) -> dict[str, Any]:
|
||||||
|
payload = item.model_dump(mode="json")
|
||||||
|
return {
|
||||||
|
"defaults": {"default_locale": payload["default_locale"]},
|
||||||
|
"tenant_capabilities": {
|
||||||
|
"allow_tenant_custom_groups": payload["allow_tenant_custom_groups"],
|
||||||
|
"allow_tenant_custom_roles": payload["allow_tenant_custom_roles"],
|
||||||
|
"allow_tenant_api_keys": payload["allow_tenant_api_keys"],
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"available_languages": payload["available_languages"],
|
||||||
|
"enabled_language_codes": payload["enabled_language_codes"],
|
||||||
|
},
|
||||||
|
"privacy_retention_policy": payload["privacy_retention_policy"],
|
||||||
|
"maintenance_mode": payload["maintenance_mode"],
|
||||||
|
"settings": payload["settings"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _record_system_settings_section_changes(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
before: dict[str, Any],
|
||||||
|
after: dict[str, Any],
|
||||||
|
principal: ApiPrincipal,
|
||||||
|
) -> None:
|
||||||
|
for section in SYSTEM_SETTINGS_SECTIONS:
|
||||||
|
if before.get(section) == after.get(section):
|
||||||
|
continue
|
||||||
|
record_change(
|
||||||
|
session,
|
||||||
|
module_id=ADMIN_MODULE_ID,
|
||||||
|
collection=SYSTEM_SETTINGS_COLLECTION,
|
||||||
|
resource_type=SYSTEM_SETTINGS_RESOURCE,
|
||||||
|
resource_id=section,
|
||||||
|
operation="updated",
|
||||||
|
actor_type="user",
|
||||||
|
actor_id=principal.user.id,
|
||||||
|
payload={"section": section},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _governance_template_item(session: Session, item: GovernanceTemplate) -> GovernanceTemplateItem:
|
def _governance_template_item(session: Session, item: GovernanceTemplate) -> GovernanceTemplateItem:
|
||||||
assignments = session.query(GovernanceTemplateAssignment).filter(
|
assignments = session.query(GovernanceTemplateAssignment).filter(
|
||||||
GovernanceTemplateAssignment.template_id == item.id
|
GovernanceTemplateAssignment.template_id == item.id
|
||||||
@@ -136,6 +305,28 @@ def _governance_template_item(session: Session, item: GovernanceTemplate) -> Gov
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_governance_template_change(session: Session, *, item: GovernanceTemplate, operation: str, principal: ApiPrincipal) -> None:
|
||||||
|
record_change(
|
||||||
|
session,
|
||||||
|
module_id=ADMIN_MODULE_ID,
|
||||||
|
collection=GOVERNANCE_TEMPLATES_COLLECTION,
|
||||||
|
resource_type=GOVERNANCE_TEMPLATE_RESOURCE,
|
||||||
|
resource_id=item.id,
|
||||||
|
operation=operation,
|
||||||
|
actor_type="user",
|
||||||
|
actor_id=principal.user.id,
|
||||||
|
payload={"kind": item.kind, "slug": item.slug, "name": item.name, "is_active": item.is_active},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _governance_template_deleted_entries(entries, visible_template_ids: set[str]):
|
||||||
|
return [
|
||||||
|
{"id": entry.resource_id, "resource_type": entry.resource_type or GOVERNANCE_TEMPLATE_RESOURCE}
|
||||||
|
for entry in entries
|
||||||
|
if entry.resource_id and (entry.operation == "deleted" or entry.resource_id not in visible_template_ids)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _module_catalog_response(session: Session, request: Request, *, notes: list[str] | None = None) -> ModuleCatalogResponse:
|
def _module_catalog_response(session: Session, request: Request, *, notes: list[str] | None = None) -> ModuleCatalogResponse:
|
||||||
registry = _request_registry(request)
|
registry = _request_registry(request)
|
||||||
current_enabled = [manifest.id for manifest in registry.manifests()]
|
current_enabled = [manifest.id for manifest in registry.manifests()]
|
||||||
@@ -251,7 +442,7 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str,
|
|||||||
result = validate_module_package_catalog()
|
result = validate_module_package_catalog()
|
||||||
if not result.get("valid"):
|
if not result.get("valid"):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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."),
|
||||||
)
|
)
|
||||||
for raw_item in result.get("modules", []):
|
for raw_item in result.get("modules", []):
|
||||||
@@ -302,6 +493,14 @@ def _module_package_catalog_item(raw_item: dict[str, object]) -> ModulePackageCa
|
|||||||
return ModulePackageCatalogItem.model_validate(payload)
|
return ModulePackageCatalogItem.model_validate(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_required_license_features(result: dict[str, object]) -> list[str]:
|
||||||
|
required: list[str] = []
|
||||||
|
for raw_item in result.get("modules", []):
|
||||||
|
if isinstance(raw_item, dict):
|
||||||
|
required.extend(_catalog_license_features(raw_item))
|
||||||
|
return list(dict.fromkeys(required))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/overview", response_model=AdminOverviewResponse)
|
@router.get("/overview", response_model=AdminOverviewResponse)
|
||||||
def admin_overview(
|
def admin_overview(
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
@@ -355,16 +554,23 @@ def read_module_install_plan(
|
|||||||
|
|
||||||
@router.get("/system/modules/install-runs", response_model=ModuleInstallerRunListResponse)
|
@router.get("/system/modules/install-runs", response_model=ModuleInstallerRunListResponse)
|
||||||
def list_module_install_runs(
|
def list_module_install_runs(
|
||||||
|
page_size: int = Query(default=DEFAULT_INSTALLER_HISTORY_PAGE_SIZE, ge=1, le=100),
|
||||||
|
cursor: str | None = None,
|
||||||
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
||||||
):
|
):
|
||||||
del principal
|
del principal
|
||||||
runtime_dir = default_installer_runtime_dir(core_settings.database_url)
|
runtime_dir = default_installer_runtime_dir(core_settings.database_url)
|
||||||
|
records = list(list_module_installer_runs(runtime_dir=runtime_dir, limit=1000))
|
||||||
|
page, next_cursor, full = _window_history_records(records, id_key="run_id", scope=INSTALLER_RUNS_CURSOR_SCOPE, page_size=page_size, cursor=cursor)
|
||||||
return ModuleInstallerRunListResponse(
|
return ModuleInstallerRunListResponse(
|
||||||
runs=[
|
runs=[
|
||||||
ModuleInstallerRunSummary.model_validate(item)
|
ModuleInstallerRunSummary.model_validate(item)
|
||||||
for item in list_module_installer_runs(runtime_dir=runtime_dir)
|
for item in page
|
||||||
],
|
],
|
||||||
lock=ModuleInstallerLockStatus.model_validate(module_installer_lock_status(runtime_dir=runtime_dir)),
|
lock=ModuleInstallerLockStatus.model_validate(module_installer_lock_status(runtime_dir=runtime_dir)),
|
||||||
|
cursor=cursor,
|
||||||
|
next_cursor=next_cursor,
|
||||||
|
full=full,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -383,16 +589,23 @@ def read_module_install_run_detail(
|
|||||||
|
|
||||||
@router.get("/system/modules/install-requests", response_model=ModuleInstallerRequestListResponse)
|
@router.get("/system/modules/install-requests", response_model=ModuleInstallerRequestListResponse)
|
||||||
def list_module_install_requests(
|
def list_module_install_requests(
|
||||||
|
page_size: int = Query(default=DEFAULT_INSTALLER_HISTORY_PAGE_SIZE, ge=1, le=100),
|
||||||
|
cursor: str | None = None,
|
||||||
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
||||||
):
|
):
|
||||||
del principal
|
del principal
|
||||||
runtime_dir = default_installer_runtime_dir(core_settings.database_url)
|
runtime_dir = default_installer_runtime_dir(core_settings.database_url)
|
||||||
|
records = list(list_module_installer_requests(runtime_dir=runtime_dir, limit=1000))
|
||||||
|
page, next_cursor, full = _window_history_records(records, id_key="request_id", scope=INSTALLER_REQUESTS_CURSOR_SCOPE, page_size=page_size, cursor=cursor)
|
||||||
return ModuleInstallerRequestListResponse(
|
return ModuleInstallerRequestListResponse(
|
||||||
requests=[
|
requests=[
|
||||||
ModuleInstallerRequestItem.model_validate(item)
|
ModuleInstallerRequestItem.model_validate(item)
|
||||||
for item in list_module_installer_requests(runtime_dir=runtime_dir)
|
for item in page
|
||||||
],
|
],
|
||||||
daemon=ModuleInstallerDaemonStatus.model_validate(module_installer_daemon_status(runtime_dir=runtime_dir)),
|
daemon=ModuleInstallerDaemonStatus.model_validate(module_installer_daemon_status(runtime_dir=runtime_dir)),
|
||||||
|
cursor=cursor,
|
||||||
|
next_cursor=next_cursor,
|
||||||
|
full=full,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -433,7 +646,12 @@ def create_module_install_request(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_request",
|
object_type="module_install_request",
|
||||||
object_id=str(request["request_id"]),
|
object_id=str(request["request_id"]),
|
||||||
details={"options": payload.options.model_dump(exclude_none=True)},
|
details=audit_operation_context(
|
||||||
|
request_id=request.get("request_id"),
|
||||||
|
outcome=request.get("status"),
|
||||||
|
trace=request.get("trace") if isinstance(request.get("trace"), dict) else None,
|
||||||
|
options=payload.options.model_dump(exclude_none=True),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return ModuleInstallerRequestItem.model_validate(request)
|
return ModuleInstallerRequestItem.model_validate(request)
|
||||||
@@ -458,7 +676,7 @@ def cancel_module_install_request(
|
|||||||
cancelled_by=principal.user.id,
|
cancelled_by=principal.user.id,
|
||||||
)
|
)
|
||||||
except ModuleInstallerError as exc:
|
except ModuleInstallerError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
audit_from_principal(
|
audit_from_principal(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
@@ -466,7 +684,12 @@ def cancel_module_install_request(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_request",
|
object_type="module_install_request",
|
||||||
object_id=request_id,
|
object_id=request_id,
|
||||||
details={},
|
details=audit_operation_context(
|
||||||
|
request_id=request_id,
|
||||||
|
outcome=request.get("status"),
|
||||||
|
trace=request.get("trace") if isinstance(request.get("trace"), dict) else None,
|
||||||
|
cancelled_by=request.get("cancelled_by"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return ModuleInstallerRequestItem.model_validate(request)
|
return ModuleInstallerRequestItem.model_validate(request)
|
||||||
@@ -491,7 +714,7 @@ def retry_module_install_request(
|
|||||||
requested_by=principal.user.id,
|
requested_by=principal.user.id,
|
||||||
)
|
)
|
||||||
except ModuleInstallerError as exc:
|
except ModuleInstallerError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
audit_from_principal(
|
audit_from_principal(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
@@ -499,7 +722,13 @@ def retry_module_install_request(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_request",
|
object_type="module_install_request",
|
||||||
object_id=request_id,
|
object_id=request_id,
|
||||||
details={"new_request_id": request["request_id"]},
|
details=audit_operation_context(
|
||||||
|
request_id=request_id,
|
||||||
|
outcome=request.get("status"),
|
||||||
|
trace=request.get("trace") if isinstance(request.get("trace"), dict) else None,
|
||||||
|
retry_of=request.get("retry_of"),
|
||||||
|
new_request_id=request.get("request_id"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return ModuleInstallerRequestItem.model_validate(request)
|
return ModuleInstallerRequestItem.model_validate(request)
|
||||||
@@ -511,6 +740,7 @@ def read_module_package_catalog(
|
|||||||
):
|
):
|
||||||
del principal
|
del principal
|
||||||
result = validate_module_package_catalog()
|
result = validate_module_package_catalog()
|
||||||
|
license_diagnostics = module_license_diagnostics(required_features=_catalog_required_license_features(result))
|
||||||
return ModulePackageCatalogResponse(
|
return ModulePackageCatalogResponse(
|
||||||
modules=[_module_package_catalog_item(item) for item in result["modules"] if isinstance(item, dict)],
|
modules=[_module_package_catalog_item(item) for item in result["modules"] if isinstance(item, dict)],
|
||||||
configured=bool(result["configured"]),
|
configured=bool(result["configured"]),
|
||||||
@@ -518,13 +748,17 @@ def read_module_package_catalog(
|
|||||||
path=result["path"] if isinstance(result["path"], str) else None,
|
path=result["path"] if isinstance(result["path"], str) else None,
|
||||||
source=result["source"] if isinstance(result.get("source"), str) else None,
|
source=result["source"] if isinstance(result.get("source"), str) else None,
|
||||||
source_type=result["source_type"] if isinstance(result.get("source_type"), str) else None,
|
source_type=result["source_type"] if isinstance(result.get("source_type"), str) else None,
|
||||||
|
cache_used=bool(result.get("cache_used")),
|
||||||
|
cache_path=result["cache_path"] if isinstance(result.get("cache_path"), str) else None,
|
||||||
channel=result["channel"] if isinstance(result["channel"], str) else None,
|
channel=result["channel"] if isinstance(result["channel"], str) else None,
|
||||||
sequence=result["sequence"] if isinstance(result.get("sequence"), int) else None,
|
sequence=result["sequence"] if isinstance(result.get("sequence"), int) else None,
|
||||||
generated_at=result["generated_at"] if isinstance(result.get("generated_at"), str) else None,
|
generated_at=result["generated_at"] if isinstance(result.get("generated_at"), str) else None,
|
||||||
|
not_before=result["not_before"] if isinstance(result.get("not_before"), str) else None,
|
||||||
expires_at=result["expires_at"] if isinstance(result.get("expires_at"), str) else None,
|
expires_at=result["expires_at"] if isinstance(result.get("expires_at"), str) else None,
|
||||||
signed=bool(result["signed"]),
|
signed=bool(result["signed"]),
|
||||||
trusted=bool(result["trusted"]),
|
trusted=bool(result["trusted"]),
|
||||||
key_id=result["key_id"] if isinstance(result["key_id"], str) else None,
|
key_id=result["key_id"] if isinstance(result["key_id"], str) else None,
|
||||||
|
license=ModuleLicenseDiagnostics.model_validate(license_diagnostics),
|
||||||
warnings=[str(item) for item in result["warnings"]] if isinstance(result["warnings"], list) else [],
|
warnings=[str(item) for item in result["warnings"]] if isinstance(result["warnings"], list) else [],
|
||||||
error=result["error"] if isinstance(result["error"], str) else None,
|
error=result["error"] if isinstance(result["error"], str) else None,
|
||||||
)
|
)
|
||||||
@@ -542,7 +776,7 @@ def plan_module_install_from_catalog(
|
|||||||
plan = _upsert_install_plan_item(session, item)
|
plan = _upsert_install_plan_item(session, item)
|
||||||
record_module_package_catalog_acceptance(validation)
|
record_module_package_catalog_acceptance(validation)
|
||||||
except ModuleManagementError as exc:
|
except ModuleManagementError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
audit_from_principal(
|
audit_from_principal(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
@@ -550,7 +784,16 @@ def plan_module_install_from_catalog(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_plan",
|
object_type="module_install_plan",
|
||||||
object_id=module_id,
|
object_id=module_id,
|
||||||
details={"items": [item.as_dict() for item in plan.items]},
|
details=audit_operation_context(
|
||||||
|
module_id=module_id,
|
||||||
|
outcome="planned",
|
||||||
|
items=[item.as_dict() for item in plan.items],
|
||||||
|
catalog_validation={
|
||||||
|
"valid": validation.get("valid"),
|
||||||
|
"channel": validation.get("channel"),
|
||||||
|
"key_id": validation.get("key_id"),
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _module_install_plan_response(session, request, notes=[f"Catalog install entry planned for {module_id}."])
|
return _module_install_plan_response(session, request, notes=[f"Catalog install entry planned for {module_id}."])
|
||||||
@@ -569,14 +812,14 @@ def plan_module_uninstall(
|
|||||||
configured_enabled = configured_enabled_modules(core_settings.enabled_modules)
|
configured_enabled = configured_enabled_modules(core_settings.enabled_modules)
|
||||||
desired_enabled = set(saved_desired_enabled_modules(session, configured_enabled))
|
desired_enabled = set(saved_desired_enabled_modules(session, configured_enabled))
|
||||||
if module_id in PROTECTED_MODULES:
|
if module_id in PROTECTED_MODULES:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Protected platform modules cannot be uninstalled.")
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Protected platform modules cannot be uninstalled.")
|
||||||
if module_id in current_enabled or module_id in desired_enabled:
|
if module_id in current_enabled or module_id in desired_enabled:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Disable the module before planning package uninstall.")
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Disable the module before planning package uninstall.")
|
||||||
try:
|
try:
|
||||||
item = module_uninstall_plan_item(module_id, lifecycle.available_modules)
|
item = module_uninstall_plan_item(module_id, lifecycle.available_modules)
|
||||||
plan = _upsert_install_plan_item(session, item)
|
plan = _upsert_install_plan_item(session, item)
|
||||||
except ModuleManagementError as exc:
|
except ModuleManagementError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
audit_from_principal(
|
audit_from_principal(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
@@ -584,7 +827,11 @@ def plan_module_uninstall(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_plan",
|
object_type="module_install_plan",
|
||||||
object_id=module_id,
|
object_id=module_id,
|
||||||
details={"items": [item.as_dict() for item in plan.items]},
|
details=audit_operation_context(
|
||||||
|
module_id=module_id,
|
||||||
|
outcome="planned",
|
||||||
|
items=[item.as_dict() for item in plan.items],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _module_install_plan_response(session, request, notes=[f"Package uninstall planned for {module_id}."])
|
return _module_install_plan_response(session, request, notes=[f"Package uninstall planned for {module_id}."])
|
||||||
@@ -599,14 +846,28 @@ def update_system_modules(
|
|||||||
):
|
):
|
||||||
lifecycle = _request_lifecycle(request)
|
lifecycle = _request_lifecycle(request)
|
||||||
available = lifecycle.available_modules
|
available = lifecycle.available_modules
|
||||||
|
before_value = {"enabled_modules": list(saved_desired_enabled_modules(session, configured_enabled_modules(core_settings.enabled_modules)))}
|
||||||
|
apply_value = {"enabled_modules": list(payload.enabled_modules)}
|
||||||
|
try:
|
||||||
|
approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="module_management.desired_enabled",
|
||||||
|
value=apply_value,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=payload.change_request_id,
|
||||||
|
target={"scope": "system"},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
try:
|
try:
|
||||||
plan = plan_desired_enabled_modules(payload.enabled_modules, available)
|
plan = plan_desired_enabled_modules(payload.enabled_modules, available)
|
||||||
except ModuleManagementError as exc:
|
except ModuleManagementError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
try:
|
try:
|
||||||
result = lifecycle.apply_enabled_modules(plan.enabled_modules, protected_modules=PROTECTED_MODULES)
|
result = lifecycle.apply_enabled_modules(plan.enabled_modules, protected_modules=PROTECTED_MODULES)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
desired = save_desired_enabled_modules(session, result.enabled_modules)
|
desired = save_desired_enabled_modules(session, result.enabled_modules)
|
||||||
audit_from_principal(
|
audit_from_principal(
|
||||||
session,
|
session,
|
||||||
@@ -615,13 +876,24 @@ def update_system_modules(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_state",
|
object_type="module_state",
|
||||||
object_id="global",
|
object_id="global",
|
||||||
details={
|
details=audit_operation_context(
|
||||||
"desired_enabled": list(desired),
|
outcome="applied",
|
||||||
"added_dependencies": list(plan.added_dependencies),
|
desired_enabled=list(desired),
|
||||||
"activated": list(result.activated_modules),
|
added_dependencies=list(plan.added_dependencies),
|
||||||
"deactivated": list(result.deactivated_modules),
|
activated=list(result.activated_modules),
|
||||||
"mounted": list(result.mounted_modules),
|
deactivated=list(result.deactivated_modules),
|
||||||
},
|
mounted=list(result.mounted_modules),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="module_management.desired_enabled",
|
||||||
|
before_value=before_value,
|
||||||
|
after_value={"enabled_modules": list(desired)},
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=approval,
|
||||||
|
target={"scope": "system"},
|
||||||
|
audit_event="module_management.updated",
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
notes = ["Module state saved and applied to the running server."]
|
notes = ["Module state saved and applied to the running server."]
|
||||||
@@ -641,10 +913,24 @@ def update_module_install_plan(
|
|||||||
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")),
|
||||||
):
|
):
|
||||||
|
before_value = {"items": [item.as_dict() for item in saved_module_install_plan(session).items]}
|
||||||
|
apply_value = {"items": [item.model_dump() for item in payload.items]}
|
||||||
|
try:
|
||||||
|
approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="module_management.install_plan",
|
||||||
|
value=apply_value,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=payload.change_request_id,
|
||||||
|
target={"scope": "system"},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
try:
|
try:
|
||||||
plan = save_module_install_plan(session, [item.model_dump() for item in payload.items])
|
plan = save_module_install_plan(session, [item.model_dump() for item in payload.items])
|
||||||
except ModuleManagementError as exc:
|
except ModuleManagementError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||||
audit_from_principal(
|
audit_from_principal(
|
||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
@@ -652,7 +938,21 @@ def update_module_install_plan(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_plan",
|
object_type="module_install_plan",
|
||||||
object_id="global",
|
object_id="global",
|
||||||
details={"items": [item.as_dict() for item in plan.items]},
|
details=audit_operation_context(
|
||||||
|
outcome="saved",
|
||||||
|
items=[item.as_dict() for item in plan.items],
|
||||||
|
item_count=len(plan.items),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="module_management.install_plan",
|
||||||
|
before_value=before_value,
|
||||||
|
after_value={"items": [item.as_dict() for item in plan.items]},
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=approval,
|
||||||
|
target={"scope": "system"},
|
||||||
|
audit_event="module_install.requested",
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _module_install_plan_response(session, request, notes=["Module package install plan saved."])
|
return _module_install_plan_response(session, request, notes=["Module package install plan saved."])
|
||||||
@@ -672,7 +972,11 @@ def clear_module_install_plan(
|
|||||||
scope="system",
|
scope="system",
|
||||||
object_type="module_install_plan",
|
object_type="module_install_plan",
|
||||||
object_id="global",
|
object_id="global",
|
||||||
details={"items": [item.as_dict() for item in plan.items]},
|
details=audit_operation_context(
|
||||||
|
outcome="cleared",
|
||||||
|
items=[item.as_dict() for item in plan.items],
|
||||||
|
item_count=len(plan.items),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _module_install_plan_response(session, request, notes=["Module package install plan cleared."])
|
return _module_install_plan_response(session, request, notes=["Module package install plan cleared."])
|
||||||
@@ -683,18 +987,59 @@ def read_system_settings(
|
|||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
||||||
):
|
):
|
||||||
item = get_system_settings(session)
|
get_system_settings(session)
|
||||||
session.commit()
|
session.commit()
|
||||||
policy = privacy_policy_from_settings(item)
|
return _system_settings_item(session)
|
||||||
maintenance_mode = saved_maintenance_mode(session)
|
|
||||||
return SystemSettingsItem(
|
|
||||||
default_locale=item.default_locale,
|
def _full_system_settings_delta_response(session: Session) -> SystemSettingsDeltaResponse:
|
||||||
allow_tenant_custom_groups=item.allow_tenant_custom_groups,
|
item = _system_settings_item(session)
|
||||||
allow_tenant_custom_roles=item.allow_tenant_custom_roles,
|
return SystemSettingsDeltaResponse(
|
||||||
allow_tenant_api_keys=item.allow_tenant_api_keys,
|
item=item,
|
||||||
privacy_retention_policy=PrivacyRetentionPolicyItem.model_validate(policy.model_dump(mode="json")),
|
sections=_system_settings_sections(item),
|
||||||
maintenance_mode=MaintenanceModeItem.model_validate(maintenance_mode.as_dict()),
|
changed_sections=list(SYSTEM_SETTINGS_SECTIONS),
|
||||||
settings=item.settings or {},
|
deleted=[],
|
||||||
|
watermark=_admin_delta_watermark(session, (SYSTEM_SETTINGS_COLLECTION,)),
|
||||||
|
has_more=False,
|
||||||
|
full=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/system/settings/delta", response_model=SystemSettingsDeltaResponse)
|
||||||
|
def read_system_settings_delta(
|
||||||
|
since: str | None = None,
|
||||||
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
|
||||||
|
):
|
||||||
|
del principal
|
||||||
|
get_system_settings(session)
|
||||||
|
session.commit()
|
||||||
|
if since is None:
|
||||||
|
return _full_system_settings_delta_response(session)
|
||||||
|
entries, has_more = _admin_delta_entries(session, collections=(SYSTEM_SETTINGS_COLLECTION,), since=since, limit=limit)
|
||||||
|
if entries is None:
|
||||||
|
return _full_system_settings_delta_response(session)
|
||||||
|
changed_sections = [
|
||||||
|
section
|
||||||
|
for section in SYSTEM_SETTINGS_SECTIONS
|
||||||
|
if section in {entry.resource_id for entry in entries if entry.resource_type == SYSTEM_SETTINGS_RESOURCE}
|
||||||
|
]
|
||||||
|
item = _system_settings_item(session)
|
||||||
|
sections = _system_settings_sections(item)
|
||||||
|
return SystemSettingsDeltaResponse(
|
||||||
|
item=None,
|
||||||
|
sections={section: sections[section] for section in changed_sections},
|
||||||
|
changed_sections=changed_sections,
|
||||||
|
deleted=[],
|
||||||
|
watermark=_admin_delta_response_watermark(
|
||||||
|
session,
|
||||||
|
collections=(SYSTEM_SETTINGS_COLLECTION,),
|
||||||
|
entries=entries,
|
||||||
|
has_more=has_more,
|
||||||
|
),
|
||||||
|
has_more=has_more,
|
||||||
|
full=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -705,10 +1050,45 @@ def write_system_settings(
|
|||||||
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
|
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
|
||||||
):
|
):
|
||||||
item = get_system_settings(session)
|
item = get_system_settings(session)
|
||||||
|
privacy_approval: ConfigurationChangeApproval | None = None
|
||||||
|
maintenance_approval: ConfigurationChangeApproval | None = None
|
||||||
|
before_sections = _system_settings_sections(_system_settings_item(session))
|
||||||
|
before_privacy = privacy_policy_from_settings(item).model_dump(mode="json")
|
||||||
|
before_maintenance = saved_maintenance_mode(session).as_dict()
|
||||||
|
if payload.privacy_retention_policy is not None:
|
||||||
|
privacy_value = payload.privacy_retention_policy.model_dump(mode="json")
|
||||||
|
try:
|
||||||
|
privacy_approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="privacy_retention_policy",
|
||||||
|
value=privacy_value,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=payload.change_request_id,
|
||||||
|
target={"scope": "system"},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
|
current_i18n = i18n_settings(item.settings)
|
||||||
|
raw_available = payload.available_languages if "available_languages" in payload.model_fields_set else current_i18n.get("available_languages")
|
||||||
|
raw_enabled = payload.enabled_language_codes if "enabled_language_codes" in payload.model_fields_set else current_i18n.get("enabled_language_codes", current_i18n.get("enabled_languages"))
|
||||||
|
available_languages = normalize_language_packages(
|
||||||
|
[item.model_dump() for item in raw_available] if payload.available_languages is not None else raw_available
|
||||||
|
)
|
||||||
|
enabled_language_codes = normalize_enabled_language_codes(
|
||||||
|
raw_enabled,
|
||||||
|
available_languages,
|
||||||
|
default_locale=payload.default_locale,
|
||||||
|
)
|
||||||
item.default_locale = payload.default_locale.strip() or "en"
|
item.default_locale = payload.default_locale.strip() or "en"
|
||||||
item.allow_tenant_custom_groups = payload.allow_tenant_custom_groups
|
item.allow_tenant_custom_groups = payload.allow_tenant_custom_groups
|
||||||
item.allow_tenant_custom_roles = payload.allow_tenant_custom_roles
|
item.allow_tenant_custom_roles = payload.allow_tenant_custom_roles
|
||||||
item.allow_tenant_api_keys = payload.allow_tenant_api_keys
|
item.allow_tenant_api_keys = payload.allow_tenant_api_keys
|
||||||
|
item.settings = update_i18n_settings(
|
||||||
|
item.settings,
|
||||||
|
available_languages=available_languages,
|
||||||
|
enabled_language_codes=enabled_language_codes,
|
||||||
|
)
|
||||||
if payload.privacy_retention_policy is not None:
|
if payload.privacy_retention_policy is not None:
|
||||||
set_privacy_policy(item, payload.privacy_retention_policy.model_dump(mode="json"))
|
set_privacy_policy(item, payload.privacy_retention_policy.model_dump(mode="json"))
|
||||||
if payload.maintenance_mode is not None:
|
if payload.maintenance_mode is not None:
|
||||||
@@ -717,9 +1097,21 @@ def write_system_settings(
|
|||||||
enabled=payload.maintenance_mode.enabled,
|
enabled=payload.maintenance_mode.enabled,
|
||||||
message=payload.maintenance_mode.message.strip() if payload.maintenance_mode.message else None,
|
message=payload.maintenance_mode.message.strip() if payload.maintenance_mode.message else None,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
maintenance_approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="maintenance_mode",
|
||||||
|
value=next_mode.as_dict(),
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=None,
|
||||||
|
target={"scope": "system"},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
if current.enabled != next_mode.enabled and not has_scope(principal, MAINTENANCE_ACCESS_SCOPE):
|
if current.enabled != next_mode.enabled and not has_scope(principal, MAINTENANCE_ACCESS_SCOPE):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
detail=f"Changing maintenance mode requires {MAINTENANCE_ACCESS_SCOPE}.",
|
detail=f"Changing maintenance mode requires {MAINTENANCE_ACCESS_SCOPE}.",
|
||||||
)
|
)
|
||||||
save_maintenance_mode(session, next_mode)
|
save_maintenance_mode(session, next_mode)
|
||||||
@@ -728,8 +1120,32 @@ def write_system_settings(
|
|||||||
session, principal, action="system_settings.updated", scope="system", object_type="system_settings", object_id=item.id,
|
session, principal, action="system_settings.updated", scope="system", object_type="system_settings", object_id=item.id,
|
||||||
details=payload.model_dump(),
|
details=payload.model_dump(),
|
||||||
)
|
)
|
||||||
|
if payload.privacy_retention_policy is not None:
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="privacy_retention_policy",
|
||||||
|
before_value=before_privacy,
|
||||||
|
after_value=privacy_policy_from_settings(item).model_dump(mode="json"),
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=privacy_approval,
|
||||||
|
target={"scope": "system"},
|
||||||
|
audit_event="privacy_retention.policy_updated",
|
||||||
|
)
|
||||||
|
if payload.maintenance_mode is not None:
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="maintenance_mode",
|
||||||
|
before_value=before_maintenance,
|
||||||
|
after_value=saved_maintenance_mode(session).as_dict(),
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=maintenance_approval,
|
||||||
|
target={"scope": "system"},
|
||||||
|
audit_event="system_settings.updated",
|
||||||
|
)
|
||||||
|
after_sections = _system_settings_sections(_system_settings_item(session))
|
||||||
|
_record_system_settings_section_changes(session, before=before_sections, after=after_sections, principal=principal)
|
||||||
session.commit()
|
session.commit()
|
||||||
return read_system_settings(session=session, principal=principal)
|
return _system_settings_item(session)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/system/governance-templates", response_model=GovernanceTemplateListResponse)
|
@router.get("/system/governance-templates", response_model=GovernanceTemplateListResponse)
|
||||||
@@ -746,12 +1162,63 @@ def list_governance_templates(
|
|||||||
return GovernanceTemplateListResponse(templates=[_governance_template_item(session, item) for item in items])
|
return GovernanceTemplateListResponse(templates=[_governance_template_item(session, item) for item in items])
|
||||||
|
|
||||||
|
|
||||||
|
def _full_governance_template_delta_response(session: Session) -> GovernanceTemplateListDeltaResponse:
|
||||||
|
items = session.query(GovernanceTemplate).order_by(GovernanceTemplate.kind.asc(), GovernanceTemplate.name.asc()).all()
|
||||||
|
return GovernanceTemplateListDeltaResponse(
|
||||||
|
templates=[_governance_template_item(session, item) for item in items],
|
||||||
|
deleted=[],
|
||||||
|
watermark=_admin_delta_watermark(session, (GOVERNANCE_TEMPLATES_COLLECTION,)),
|
||||||
|
has_more=False,
|
||||||
|
full=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/system/governance-templates/delta", response_model=GovernanceTemplateListDeltaResponse)
|
||||||
|
def list_governance_templates_delta(
|
||||||
|
since: str | None = None,
|
||||||
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
principal: ApiPrincipal = Depends(require_scope("system:governance:read")),
|
||||||
|
):
|
||||||
|
del principal
|
||||||
|
if since is None:
|
||||||
|
return _full_governance_template_delta_response(session)
|
||||||
|
entries, has_more = _admin_delta_entries(session, collections=(GOVERNANCE_TEMPLATES_COLLECTION,), since=since, limit=limit)
|
||||||
|
if entries is None:
|
||||||
|
return _full_governance_template_delta_response(session)
|
||||||
|
changed_ids = [entry.resource_id for entry in entries if entry.resource_id and entry.operation != "deleted"]
|
||||||
|
items = []
|
||||||
|
if changed_ids:
|
||||||
|
items = session.query(GovernanceTemplate).filter(GovernanceTemplate.id.in_(changed_ids)).order_by(GovernanceTemplate.kind.asc(), GovernanceTemplate.name.asc()).all()
|
||||||
|
visible_template_ids = {item.id for item in items}
|
||||||
|
return GovernanceTemplateListDeltaResponse(
|
||||||
|
templates=[_governance_template_item(session, item) for item in items],
|
||||||
|
deleted=_governance_template_deleted_entries(entries, visible_template_ids),
|
||||||
|
watermark=_admin_delta_response_watermark(session, collections=(GOVERNANCE_TEMPLATES_COLLECTION,), entries=entries, has_more=has_more),
|
||||||
|
has_more=has_more,
|
||||||
|
full=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/system/governance-templates", response_model=GovernanceTemplateItem, status_code=status.HTTP_201_CREATED)
|
@router.post("/system/governance-templates", response_model=GovernanceTemplateItem, status_code=status.HTTP_201_CREATED)
|
||||||
def create_governance_template(
|
def create_governance_template(
|
||||||
payload: GovernanceTemplateCreateRequest,
|
payload: GovernanceTemplateCreateRequest,
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
|
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
|
||||||
):
|
):
|
||||||
|
apply_value = payload.model_dump(exclude={"change_request_id"})
|
||||||
|
try:
|
||||||
|
approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="governance_templates",
|
||||||
|
value=apply_value,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=payload.change_request_id,
|
||||||
|
target={"kind": payload.kind, "slug": payload.slug},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
try:
|
try:
|
||||||
item = create_template(
|
item = create_template(
|
||||||
session,
|
session,
|
||||||
@@ -774,6 +1241,17 @@ def create_governance_template(
|
|||||||
object_id=item.id,
|
object_id=item.id,
|
||||||
details={"slug": item.slug, "tenant_ids": [assignment.tenant_id for assignment in payload.assignments]},
|
details={"slug": item.slug, "tenant_ids": [assignment.tenant_id for assignment in payload.assignments]},
|
||||||
)
|
)
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="governance_templates",
|
||||||
|
before_value=None,
|
||||||
|
after_value={**apply_value, "id": item.id},
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=approval,
|
||||||
|
target={"kind": item.kind, "id": item.id},
|
||||||
|
audit_event="governance_template.updated",
|
||||||
|
)
|
||||||
|
_record_governance_template_change(session, item=item, operation="created", principal=principal)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _governance_template_item(session, item)
|
return _governance_template_item(session, item)
|
||||||
|
|
||||||
@@ -788,6 +1266,20 @@ def update_governance_template(
|
|||||||
item = session.get(GovernanceTemplate, template_id)
|
item = session.get(GovernanceTemplate, template_id)
|
||||||
if item is None:
|
if item is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Governance template not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Governance template not found")
|
||||||
|
before_value = _governance_template_item(session, item).model_dump(mode="json")
|
||||||
|
apply_value = payload.model_dump(exclude={"change_request_id"})
|
||||||
|
try:
|
||||||
|
approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="governance_templates",
|
||||||
|
value=apply_value,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=payload.change_request_id,
|
||||||
|
target={"kind": item.kind, "id": item.id},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
try:
|
try:
|
||||||
update_template(
|
update_template(
|
||||||
session,
|
session,
|
||||||
@@ -809,6 +1301,17 @@ def update_governance_template(
|
|||||||
object_id=item.id,
|
object_id=item.id,
|
||||||
details={"tenant_ids": [assignment.tenant_id for assignment in payload.assignments]},
|
details={"tenant_ids": [assignment.tenant_id for assignment in payload.assignments]},
|
||||||
)
|
)
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="governance_templates",
|
||||||
|
before_value=before_value,
|
||||||
|
after_value=_governance_template_item(session, item).model_dump(mode="json"),
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=approval,
|
||||||
|
target={"kind": item.kind, "id": item.id},
|
||||||
|
audit_event="governance_template.updated",
|
||||||
|
)
|
||||||
|
_record_governance_template_change(session, item=item, operation="updated", principal=principal)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _governance_template_item(session, item)
|
return _governance_template_item(session, item)
|
||||||
|
|
||||||
@@ -816,6 +1319,7 @@ def update_governance_template(
|
|||||||
@router.delete("/system/governance-templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/system/governance-templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
def remove_governance_template(
|
def remove_governance_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
|
change_request_id: str | None = Query(default=None),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
|
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
|
||||||
):
|
):
|
||||||
@@ -823,6 +1327,20 @@ def remove_governance_template(
|
|||||||
if item is None:
|
if item is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Governance template not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Governance template not found")
|
||||||
kind = item.kind
|
kind = item.kind
|
||||||
|
before_value = _governance_template_item(session, item).model_dump(mode="json")
|
||||||
|
apply_value = {"id": item.id, "kind": item.kind, "delete": True}
|
||||||
|
try:
|
||||||
|
approval = ensure_configuration_change_allowed(
|
||||||
|
session,
|
||||||
|
key="governance_templates",
|
||||||
|
value=apply_value,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
actor_scopes=tuple(principal.scopes),
|
||||||
|
change_request_id=change_request_id,
|
||||||
|
target={"kind": item.kind, "id": item.id},
|
||||||
|
)
|
||||||
|
except ConfigurationControlError as exc:
|
||||||
|
raise _configuration_control_http_error(exc) from exc
|
||||||
try:
|
try:
|
||||||
delete_template(session, item)
|
delete_template(session, item)
|
||||||
except (AdminConflictError, AdminValidationError) as exc:
|
except (AdminConflictError, AdminValidationError) as exc:
|
||||||
@@ -836,5 +1354,16 @@ def remove_governance_template(
|
|||||||
object_id=template_id,
|
object_id=template_id,
|
||||||
details={},
|
details={},
|
||||||
)
|
)
|
||||||
|
record_configuration_change_applied(
|
||||||
|
session,
|
||||||
|
key="governance_templates",
|
||||||
|
before_value=before_value,
|
||||||
|
after_value=None,
|
||||||
|
actor_user_id=principal.user.id,
|
||||||
|
approval=approval,
|
||||||
|
target={"kind": kind, "id": template_id},
|
||||||
|
audit_event="governance_template.updated",
|
||||||
|
)
|
||||||
|
_record_governance_template_change(session, item=item, operation="deleted", principal=principal)
|
||||||
session.commit()
|
session.commit()
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from typing import Any, Literal
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||||
|
|
||||||
|
|
||||||
RETENTION_DAY_KEYS = (
|
RETENTION_DAY_KEYS = (
|
||||||
"raw_campaign_json_retention_days",
|
"raw_campaign_json_retention_days",
|
||||||
@@ -65,6 +67,14 @@ class MaintenanceModeItem(BaseModel):
|
|||||||
message: str | None = Field(default=None, max_length=500)
|
message: str | None = Field(default=None, max_length=500)
|
||||||
|
|
||||||
|
|
||||||
|
class LanguagePackageItem(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
code: str = Field(min_length=1, max_length=20)
|
||||||
|
label: str = Field(min_length=1, max_length=100)
|
||||||
|
native_label: str | None = Field(default=None, max_length=100)
|
||||||
|
|
||||||
|
|
||||||
class AdminOverviewResponse(BaseModel):
|
class AdminOverviewResponse(BaseModel):
|
||||||
active_tenant_id: str
|
active_tenant_id: str
|
||||||
active_tenant_name: str
|
active_tenant_name: str
|
||||||
@@ -87,9 +97,21 @@ class SystemSettingsItem(BaseModel):
|
|||||||
allow_tenant_api_keys: bool = True
|
allow_tenant_api_keys: bool = True
|
||||||
privacy_retention_policy: PrivacyRetentionPolicyItem = Field(default_factory=PrivacyRetentionPolicyItem)
|
privacy_retention_policy: PrivacyRetentionPolicyItem = Field(default_factory=PrivacyRetentionPolicyItem)
|
||||||
maintenance_mode: MaintenanceModeItem = Field(default_factory=MaintenanceModeItem)
|
maintenance_mode: MaintenanceModeItem = Field(default_factory=MaintenanceModeItem)
|
||||||
|
available_languages: list[LanguagePackageItem] = Field(default_factory=list)
|
||||||
|
enabled_language_codes: list[str] = Field(default_factory=list)
|
||||||
settings: dict[str, Any] = Field(default_factory=dict)
|
settings: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemSettingsDeltaResponse(BaseModel):
|
||||||
|
item: SystemSettingsItem | None = None
|
||||||
|
sections: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
changed_sections: list[str] = Field(default_factory=list)
|
||||||
|
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
||||||
|
watermark: str | None = None
|
||||||
|
has_more: bool = False
|
||||||
|
full: bool = False
|
||||||
|
|
||||||
|
|
||||||
class SystemSettingsUpdateRequest(BaseModel):
|
class SystemSettingsUpdateRequest(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
@@ -99,6 +121,9 @@ class SystemSettingsUpdateRequest(BaseModel):
|
|||||||
allow_tenant_api_keys: bool
|
allow_tenant_api_keys: bool
|
||||||
privacy_retention_policy: PrivacyRetentionPolicyItem | None = None
|
privacy_retention_policy: PrivacyRetentionPolicyItem | None = None
|
||||||
maintenance_mode: MaintenanceModeItem | None = None
|
maintenance_mode: MaintenanceModeItem | None = None
|
||||||
|
available_languages: list[LanguagePackageItem] | None = None
|
||||||
|
enabled_language_codes: list[str] | None = None
|
||||||
|
change_request_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ModuleCatalogItem(BaseModel):
|
class ModuleCatalogItem(BaseModel):
|
||||||
@@ -143,6 +168,7 @@ class ModuleStateUpdateRequest(BaseModel):
|
|||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
enabled_modules: list[str] = Field(default_factory=list)
|
enabled_modules: list[str] = Field(default_factory=list)
|
||||||
|
change_request_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ModuleInstallPlanItem(BaseModel):
|
class ModuleInstallPlanItem(BaseModel):
|
||||||
@@ -213,6 +239,9 @@ class ModuleInstallerRunSummary(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
started_at: str | None = None
|
started_at: str | None = None
|
||||||
finished_at: str | None = None
|
finished_at: str | None = None
|
||||||
|
request_id: str | None = None
|
||||||
|
requested_by: str | None = None
|
||||||
|
trace: dict[str, Any] | None = None
|
||||||
rollback_status: str | None = None
|
rollback_status: str | None = None
|
||||||
supervisor_status: str | None = None
|
supervisor_status: str | None = None
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
@@ -224,6 +253,9 @@ class ModuleInstallerRunSummary(BaseModel):
|
|||||||
class ModuleInstallerRunListResponse(BaseModel):
|
class ModuleInstallerRunListResponse(BaseModel):
|
||||||
runs: list[ModuleInstallerRunSummary] = Field(default_factory=list)
|
runs: list[ModuleInstallerRunSummary] = Field(default_factory=list)
|
||||||
lock: ModuleInstallerLockStatus
|
lock: ModuleInstallerLockStatus
|
||||||
|
cursor: str | None = None
|
||||||
|
next_cursor: str | None = None
|
||||||
|
full: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ModuleInstallerRequestOptions(BaseModel):
|
class ModuleInstallerRequestOptions(BaseModel):
|
||||||
@@ -256,6 +288,7 @@ class ModuleInstallerRequestItem(BaseModel):
|
|||||||
cancelled_at: str | None = None
|
cancelled_at: str | None = None
|
||||||
cancelled_by: str | None = None
|
cancelled_by: str | None = None
|
||||||
retry_of: str | None = None
|
retry_of: str | None = None
|
||||||
|
trace: dict[str, Any] | None = None
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
record_path: str | None = None
|
record_path: str | None = None
|
||||||
options: dict[str, Any] = Field(default_factory=dict)
|
options: dict[str, Any] = Field(default_factory=dict)
|
||||||
@@ -264,6 +297,9 @@ class ModuleInstallerRequestItem(BaseModel):
|
|||||||
class ModuleInstallerRequestListResponse(BaseModel):
|
class ModuleInstallerRequestListResponse(BaseModel):
|
||||||
requests: list[ModuleInstallerRequestItem] = Field(default_factory=list)
|
requests: list[ModuleInstallerRequestItem] = Field(default_factory=list)
|
||||||
daemon: ModuleInstallerDaemonStatus
|
daemon: ModuleInstallerDaemonStatus
|
||||||
|
cursor: str | None = None
|
||||||
|
next_cursor: str | None = None
|
||||||
|
full: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ModuleInstallerRequestCreateRequest(BaseModel):
|
class ModuleInstallerRequestCreateRequest(BaseModel):
|
||||||
@@ -291,6 +327,27 @@ class ModulePackageCatalogItem(BaseModel):
|
|||||||
tags: list[str] = Field(default_factory=list)
|
tags: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleLicenseDiagnostics(BaseModel):
|
||||||
|
configured: bool = False
|
||||||
|
valid: bool = True
|
||||||
|
path: str | None = None
|
||||||
|
license_id: str | None = None
|
||||||
|
subject: str | None = None
|
||||||
|
features: list[str] = Field(default_factory=list)
|
||||||
|
valid_from: str | None = None
|
||||||
|
valid_until: str | None = None
|
||||||
|
signed: bool = False
|
||||||
|
trusted: bool = False
|
||||||
|
key_id: str | None = None
|
||||||
|
enforced: bool = False
|
||||||
|
allowed: bool = True
|
||||||
|
required_features: list[str] = Field(default_factory=list)
|
||||||
|
missing_features: list[str] = Field(default_factory=list)
|
||||||
|
expires_in_days: int | None = None
|
||||||
|
reason: str | None = None
|
||||||
|
guidance: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class ModulePackageCatalogResponse(BaseModel):
|
class ModulePackageCatalogResponse(BaseModel):
|
||||||
modules: list[ModulePackageCatalogItem] = Field(default_factory=list)
|
modules: list[ModulePackageCatalogItem] = Field(default_factory=list)
|
||||||
configured: bool = False
|
configured: bool = False
|
||||||
@@ -298,13 +355,17 @@ class ModulePackageCatalogResponse(BaseModel):
|
|||||||
path: str | None = None
|
path: str | None = None
|
||||||
source: str | None = None
|
source: str | None = None
|
||||||
source_type: str | None = None
|
source_type: str | None = None
|
||||||
|
cache_used: bool = False
|
||||||
|
cache_path: str | None = None
|
||||||
channel: str | None = None
|
channel: str | None = None
|
||||||
sequence: int | None = None
|
sequence: int | None = None
|
||||||
generated_at: str | None = None
|
generated_at: str | None = None
|
||||||
|
not_before: str | None = None
|
||||||
expires_at: str | None = None
|
expires_at: str | None = None
|
||||||
signed: bool = False
|
signed: bool = False
|
||||||
trusted: bool = False
|
trusted: bool = False
|
||||||
key_id: str | None = None
|
key_id: str | None = None
|
||||||
|
license: ModuleLicenseDiagnostics = Field(default_factory=ModuleLicenseDiagnostics)
|
||||||
warnings: list[str] = Field(default_factory=list)
|
warnings: list[str] = Field(default_factory=list)
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
|
|
||||||
@@ -313,6 +374,7 @@ class ModuleInstallPlanUpdateRequest(BaseModel):
|
|||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
items: list[ModuleInstallPlanItem] = Field(default_factory=list)
|
items: list[ModuleInstallPlanItem] = Field(default_factory=list)
|
||||||
|
change_request_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class GovernanceAssignment(BaseModel):
|
class GovernanceAssignment(BaseModel):
|
||||||
@@ -338,6 +400,14 @@ class GovernanceTemplateListResponse(BaseModel):
|
|||||||
templates: list[GovernanceTemplateItem]
|
templates: list[GovernanceTemplateItem]
|
||||||
|
|
||||||
|
|
||||||
|
class GovernanceTemplateListDeltaResponse(BaseModel):
|
||||||
|
templates: list[GovernanceTemplateItem] = Field(default_factory=list)
|
||||||
|
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
||||||
|
watermark: str | None = None
|
||||||
|
has_more: bool = False
|
||||||
|
full: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GovernanceTemplateCreateRequest(BaseModel):
|
class GovernanceTemplateCreateRequest(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
@@ -348,6 +418,7 @@ class GovernanceTemplateCreateRequest(BaseModel):
|
|||||||
permissions: list[str] = Field(default_factory=list)
|
permissions: list[str] = Field(default_factory=list)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
assignments: list[GovernanceAssignment] = Field(default_factory=list)
|
assignments: list[GovernanceAssignment] = Field(default_factory=list)
|
||||||
|
change_request_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class GovernanceTemplateUpdateRequest(BaseModel):
|
class GovernanceTemplateUpdateRequest(BaseModel):
|
||||||
@@ -358,3 +429,4 @@ class GovernanceTemplateUpdateRequest(BaseModel):
|
|||||||
permissions: list[str] = Field(default_factory=list)
|
permissions: list[str] = Field(default_factory=list)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
assignments: list[GovernanceAssignment] = Field(default_factory=list)
|
assignments: list[GovernanceAssignment] = Field(default_factory=list)
|
||||||
|
change_request_id: str | None = None
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,7 +13,7 @@ def new_uuid() -> str:
|
|||||||
|
|
||||||
|
|
||||||
class GovernanceTemplate(Base, TimestampMixin):
|
class GovernanceTemplate(Base, TimestampMixin):
|
||||||
__tablename__ = "governance_templates"
|
__tablename__ = "admin_governance_templates"
|
||||||
__table_args__ = (UniqueConstraint("kind", "slug", name="uq_governance_templates_kind_slug"),)
|
__table_args__ = (UniqueConstraint("kind", "slug", name="uq_governance_templates_kind_slug"),)
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||||
@@ -26,12 +26,12 @@ class GovernanceTemplate(Base, TimestampMixin):
|
|||||||
|
|
||||||
|
|
||||||
class GovernanceTemplateAssignment(Base, TimestampMixin):
|
class GovernanceTemplateAssignment(Base, TimestampMixin):
|
||||||
__tablename__ = "governance_template_assignments"
|
__tablename__ = "admin_governance_template_assignments"
|
||||||
__table_args__ = (UniqueConstraint("template_id", "tenant_id", name="uq_governance_template_tenant"),)
|
__table_args__ = (UniqueConstraint("template_id", "tenant_id", name="uq_governance_template_tenant"),)
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||||
template_id: Mapped[str] = mapped_column(ForeignKey("governance_templates.id", ondelete="CASCADE"), nullable=False, index=True)
|
template_id: Mapped[str] = mapped_column(ForeignKey("admin_governance_templates.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
mode: Mapped[str] = mapped_column(String(20), default="available", nullable=False)
|
mode: Mapped[str] = mapped_column(String(20), default="available", nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.6",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ApiSettings } from "@govoplan/core-webui";
|
import type { ApiSettings, DeltaDeletedItem } from "@govoplan/core-webui";
|
||||||
import { apiFetch } from "@govoplan/core-webui";
|
import { apiFetch } from "@govoplan/core-webui";
|
||||||
|
|
||||||
export type PermissionItem = {
|
export type PermissionItem = {
|
||||||
@@ -75,6 +75,12 @@ export type MaintenanceMode = {
|
|||||||
message?: string | null;
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LanguagePackage = {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
native_label?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type SystemSettingsItem = {
|
export type SystemSettingsItem = {
|
||||||
default_locale: string;
|
default_locale: string;
|
||||||
allow_tenant_custom_groups: boolean;
|
allow_tenant_custom_groups: boolean;
|
||||||
@@ -82,9 +88,20 @@ export type SystemSettingsItem = {
|
|||||||
allow_tenant_api_keys: boolean;
|
allow_tenant_api_keys: boolean;
|
||||||
privacy_retention_policy: PrivacyRetentionPolicy;
|
privacy_retention_policy: PrivacyRetentionPolicy;
|
||||||
maintenance_mode: MaintenanceMode;
|
maintenance_mode: MaintenanceMode;
|
||||||
|
available_languages: LanguagePackage[];
|
||||||
|
enabled_language_codes: string[];
|
||||||
settings: Record<string, unknown>;
|
settings: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SystemSettingsDeltaSections = Partial<{
|
||||||
|
defaults: Pick<SystemSettingsItem, "default_locale">;
|
||||||
|
tenant_capabilities: Pick<SystemSettingsItem, "allow_tenant_custom_groups" | "allow_tenant_custom_roles" | "allow_tenant_api_keys">;
|
||||||
|
languages: Pick<SystemSettingsItem, "available_languages" | "enabled_language_codes">;
|
||||||
|
privacy_retention_policy: SystemSettingsItem["privacy_retention_policy"];
|
||||||
|
maintenance_mode: SystemSettingsItem["maintenance_mode"];
|
||||||
|
settings: SystemSettingsItem["settings"];
|
||||||
|
}>;
|
||||||
|
|
||||||
export type SystemSettingsUpdatePayload = {
|
export type SystemSettingsUpdatePayload = {
|
||||||
default_locale: string;
|
default_locale: string;
|
||||||
allow_tenant_custom_groups: boolean;
|
allow_tenant_custom_groups: boolean;
|
||||||
@@ -92,8 +109,106 @@ export type SystemSettingsUpdatePayload = {
|
|||||||
allow_tenant_api_keys: boolean;
|
allow_tenant_api_keys: boolean;
|
||||||
privacy_retention_policy?: PrivacyRetentionPolicy | null;
|
privacy_retention_policy?: PrivacyRetentionPolicy | null;
|
||||||
maintenance_mode?: MaintenanceMode | null;
|
maintenance_mode?: MaintenanceMode | null;
|
||||||
|
available_languages?: LanguagePackage[] | null;
|
||||||
|
enabled_language_codes?: string[] | null;
|
||||||
|
change_request_id?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ConfigurationChangeRequest = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
label?: string;
|
||||||
|
target?: Record<string, unknown>;
|
||||||
|
dry_run: boolean;
|
||||||
|
requested_by: string;
|
||||||
|
requested_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
status: string;
|
||||||
|
approvals: Array<Record<string, unknown>>;
|
||||||
|
plan: Record<string, unknown>;
|
||||||
|
value_preview?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigurationChangeRecord = {
|
||||||
|
id: string;
|
||||||
|
version: number;
|
||||||
|
key: string;
|
||||||
|
target?: Record<string, unknown>;
|
||||||
|
actor_user_id: string;
|
||||||
|
approval_request_id?: string | null;
|
||||||
|
approval_user_ids: string[];
|
||||||
|
before?: unknown;
|
||||||
|
after?: unknown;
|
||||||
|
rollback_value?: unknown;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigurationPackageDiagnostic = {
|
||||||
|
severity: "blocker" | "warning" | "info";
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
module_id?: string | null;
|
||||||
|
object_ref?: string | null;
|
||||||
|
resolution?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigurationPackageRequiredData = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
data_type: string;
|
||||||
|
required: boolean;
|
||||||
|
secret: boolean;
|
||||||
|
description?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigurationPackagePlanItem = {
|
||||||
|
action: "create" | "update" | "bind" | "skip" | "blocked" | "noop";
|
||||||
|
module_id: string;
|
||||||
|
fragment_type: string;
|
||||||
|
fragment_id?: string | null;
|
||||||
|
summary?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigurationPackageFragment = {
|
||||||
|
module_id: string;
|
||||||
|
fragment_type: string;
|
||||||
|
fragment_id?: string | null;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigurationPackageRunPayload = {
|
||||||
|
package: Record<string, unknown>;
|
||||||
|
tenant_id?: string | null;
|
||||||
|
supplied_data?: Record<string, unknown>;
|
||||||
|
change_request_id?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeltaResponseFields = {
|
||||||
|
deleted: DeltaDeletedItem[];
|
||||||
|
watermark?: string | null;
|
||||||
|
has_more: boolean;
|
||||||
|
full: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.since) params.set("since", options.since);
|
||||||
|
if (options.limit) params.set("limit", String(options.limit));
|
||||||
|
return params.toString() ? `?${params.toString()}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemSettingsDeltaResponse = {
|
||||||
|
item?: SystemSettingsItem | null;
|
||||||
|
sections: SystemSettingsDeltaSections;
|
||||||
|
changed_sections: string[];
|
||||||
|
} & DeltaResponseFields;
|
||||||
|
|
||||||
|
export type ConfigurationChangesDeltaResponse = {
|
||||||
|
requests: ConfigurationChangeRequest[];
|
||||||
|
history: ConfigurationChangeRecord[];
|
||||||
|
} & DeltaResponseFields;
|
||||||
|
|
||||||
export type ModuleCatalogItem = {
|
export type ModuleCatalogItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -196,6 +311,9 @@ export type ModuleInstallerRunSummary = {
|
|||||||
status: string;
|
status: string;
|
||||||
started_at?: string | null;
|
started_at?: string | null;
|
||||||
finished_at?: string | null;
|
finished_at?: string | null;
|
||||||
|
request_id?: string | null;
|
||||||
|
requested_by?: string | null;
|
||||||
|
trace?: Record<string, unknown> | null;
|
||||||
rollback_status?: string | null;
|
rollback_status?: string | null;
|
||||||
supervisor_status?: string | null;
|
supervisor_status?: string | null;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
@@ -207,6 +325,9 @@ export type ModuleInstallerRunSummary = {
|
|||||||
export type ModuleInstallerRunListResponse = {
|
export type ModuleInstallerRunListResponse = {
|
||||||
runs: ModuleInstallerRunSummary[];
|
runs: ModuleInstallerRunSummary[];
|
||||||
lock: ModuleInstallerLockStatus;
|
lock: ModuleInstallerLockStatus;
|
||||||
|
cursor?: string | null;
|
||||||
|
next_cursor?: string | null;
|
||||||
|
full?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModuleInstallerRequestOptions = {
|
export type ModuleInstallerRequestOptions = {
|
||||||
@@ -235,6 +356,7 @@ export type ModuleInstallerRequestItem = {
|
|||||||
cancelled_at?: string | null;
|
cancelled_at?: string | null;
|
||||||
cancelled_by?: string | null;
|
cancelled_by?: string | null;
|
||||||
retry_of?: string | null;
|
retry_of?: string | null;
|
||||||
|
trace?: Record<string, unknown> | null;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
record_path?: string | null;
|
record_path?: string | null;
|
||||||
options: Record<string, unknown>;
|
options: Record<string, unknown>;
|
||||||
@@ -243,6 +365,9 @@ export type ModuleInstallerRequestItem = {
|
|||||||
export type ModuleInstallerRequestListResponse = {
|
export type ModuleInstallerRequestListResponse = {
|
||||||
requests: ModuleInstallerRequestItem[];
|
requests: ModuleInstallerRequestItem[];
|
||||||
daemon: ModuleInstallerDaemonStatus;
|
daemon: ModuleInstallerDaemonStatus;
|
||||||
|
cursor?: string | null;
|
||||||
|
next_cursor?: string | null;
|
||||||
|
full?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModulePackageCatalogItem = {
|
export type ModulePackageCatalogItem = {
|
||||||
@@ -264,6 +389,27 @@ export type ModulePackageCatalogItem = {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ModuleLicenseDiagnostics = {
|
||||||
|
configured: boolean;
|
||||||
|
valid: boolean;
|
||||||
|
path?: string | null;
|
||||||
|
license_id?: string | null;
|
||||||
|
subject?: string | null;
|
||||||
|
features: string[];
|
||||||
|
valid_from?: string | null;
|
||||||
|
valid_until?: string | null;
|
||||||
|
signed: boolean;
|
||||||
|
trusted: boolean;
|
||||||
|
key_id?: string | null;
|
||||||
|
enforced: boolean;
|
||||||
|
allowed: boolean;
|
||||||
|
required_features: string[];
|
||||||
|
missing_features: string[];
|
||||||
|
expires_in_days?: number | null;
|
||||||
|
reason?: string | null;
|
||||||
|
guidance: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type ModulePackageCatalogResponse = {
|
export type ModulePackageCatalogResponse = {
|
||||||
modules: ModulePackageCatalogItem[];
|
modules: ModulePackageCatalogItem[];
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
@@ -271,13 +417,17 @@ export type ModulePackageCatalogResponse = {
|
|||||||
path?: string | null;
|
path?: string | null;
|
||||||
source?: string | null;
|
source?: string | null;
|
||||||
source_type?: string | null;
|
source_type?: string | null;
|
||||||
|
cache_used: boolean;
|
||||||
|
cache_path?: string | null;
|
||||||
channel?: string | null;
|
channel?: string | null;
|
||||||
sequence?: number | null;
|
sequence?: number | null;
|
||||||
generated_at?: string | null;
|
generated_at?: string | null;
|
||||||
|
not_before?: string | null;
|
||||||
expires_at?: string | null;
|
expires_at?: string | null;
|
||||||
signed: boolean;
|
signed: boolean;
|
||||||
trusted: boolean;
|
trusted: boolean;
|
||||||
key_id?: string | null;
|
key_id?: string | null;
|
||||||
|
license: ModuleLicenseDiagnostics;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
};
|
};
|
||||||
@@ -319,6 +469,11 @@ export function fetchSystemSettings(settings: ApiSettings): Promise<SystemSettin
|
|||||||
return apiFetch(settings, "/api/v1/admin/system/settings");
|
return apiFetch(settings, "/api/v1/admin/system/settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchSystemSettingsDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<SystemSettingsDeltaResponse> {
|
||||||
|
const suffix = deltaSuffix(options);
|
||||||
|
return apiFetch(settings, `/api/v1/admin/system/settings/delta${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateSystemSettings(settings: ApiSettings, payload: SystemSettingsUpdatePayload): Promise<SystemSettingsItem> {
|
export function updateSystemSettings(settings: ApiSettings, payload: SystemSettingsUpdatePayload): Promise<SystemSettingsItem> {
|
||||||
return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) });
|
return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) });
|
||||||
}
|
}
|
||||||
@@ -327,10 +482,10 @@ export function fetchModuleCatalog(settings: ApiSettings): Promise<ModuleCatalog
|
|||||||
return apiFetch(settings, "/api/v1/admin/system/modules");
|
return apiFetch(settings, "/api/v1/admin/system/modules");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateModuleState(settings: ApiSettings, enabledModules: string[]): Promise<ModuleCatalogResponse> {
|
export function updateModuleState(settings: ApiSettings, enabledModules: string[], changeRequestId?: string | null): Promise<ModuleCatalogResponse> {
|
||||||
return apiFetch(settings, "/api/v1/admin/system/modules", {
|
return apiFetch(settings, "/api/v1/admin/system/modules", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({ enabled_modules: enabledModules })
|
body: JSON.stringify({ enabled_modules: enabledModules, change_request_id: changeRequestId ?? null })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,12 +493,20 @@ export function fetchModuleInstallPlan(settings: ApiSettings): Promise<ModuleIns
|
|||||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan");
|
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchModuleInstallerRuns(settings: ApiSettings): Promise<ModuleInstallerRunListResponse> {
|
export function fetchModuleInstallerRuns(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRunListResponse> {
|
||||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-runs");
|
const params = new URLSearchParams();
|
||||||
|
if (options.page_size) params.set("page_size", String(options.page_size));
|
||||||
|
if (options.cursor) params.set("cursor", options.cursor);
|
||||||
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
||||||
|
return apiFetch(settings, `/api/v1/admin/system/modules/install-runs${suffix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchModuleInstallerRequests(settings: ApiSettings): Promise<ModuleInstallerRequestListResponse> {
|
export function fetchModuleInstallerRequests(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRequestListResponse> {
|
||||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-requests");
|
const params = new URLSearchParams();
|
||||||
|
if (options.page_size) params.set("page_size", String(options.page_size));
|
||||||
|
if (options.cursor) params.set("cursor", options.cursor);
|
||||||
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
||||||
|
return apiFetch(settings, `/api/v1/admin/system/modules/install-requests${suffix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createModuleInstallerRequest(settings: ApiSettings, options: ModuleInstallerRequestOptions): Promise<ModuleInstallerRequestItem> {
|
export function createModuleInstallerRequest(settings: ApiSettings, options: ModuleInstallerRequestOptions): Promise<ModuleInstallerRequestItem> {
|
||||||
@@ -373,10 +536,10 @@ export function planModuleUninstall(settings: ApiSettings, moduleId: string): Pr
|
|||||||
return apiFetch(settings, `/api/v1/admin/system/modules/${encodeURIComponent(moduleId)}/uninstall-plan`, { method: "POST" });
|
return apiFetch(settings, `/api/v1/admin/system/modules/${encodeURIComponent(moduleId)}/uninstall-plan`, { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateModuleInstallPlan(settings: ApiSettings, items: ModuleInstallPlanItem[]): Promise<ModuleInstallPlanResponse> {
|
export function updateModuleInstallPlan(settings: ApiSettings, items: ModuleInstallPlanItem[], changeRequestId?: string | null): Promise<ModuleInstallPlanResponse> {
|
||||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan", {
|
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({ items })
|
body: JSON.stringify({ items, change_request_id: changeRequestId ?? null })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,14 +553,88 @@ export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "gr
|
|||||||
return response.templates;
|
return response.templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGovernanceTemplate(settings: ApiSettings, payload: Omit<GovernanceTemplateItem, "id" | "created_at" | "updated_at" | "effective_permission_count">): 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> {
|
||||||
return apiFetch(settings, "/api/v1/admin/system/governance-templates", { method: "POST", body: JSON.stringify(payload) });
|
return apiFetch(settings, "/api/v1/admin/system/governance-templates", { method: "POST", body: JSON.stringify(payload) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateGovernanceTemplate(settings: ApiSettings, templateId: string, payload: Omit<GovernanceTemplateItem, "id" | "kind" | "slug" | "created_at" | "updated_at" | "effective_permission_count">): Promise<GovernanceTemplateItem> {
|
export function updateGovernanceTemplate(settings: ApiSettings, templateId: string, payload: Omit<GovernanceTemplateItem, "id" | "kind" | "slug" | "created_at" | "updated_at" | "effective_permission_count"> & { change_request_id?: string | null }): Promise<GovernanceTemplateItem> {
|
||||||
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}`, { method: "PATCH", body: JSON.stringify(payload) });
|
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}`, { method: "PATCH", body: JSON.stringify(payload) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string): Promise<void> {
|
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string, changeRequestId?: string | null): Promise<void> {
|
||||||
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}`, { method: "DELETE" });
|
const suffix = changeRequestId ? `?change_request_id=${encodeURIComponent(changeRequestId)}` : "";
|
||||||
|
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}${suffix}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> {
|
||||||
|
return apiFetch(settings, "/api/v1/admin/configuration-changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchConfigurationChangesDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<ConfigurationChangesDeltaResponse> {
|
||||||
|
const suffix = deltaSuffix(options);
|
||||||
|
return apiFetch(settings, `/api/v1/admin/configuration-changes/delta${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createConfigurationChangeRequest(settings: ApiSettings, payload: {
|
||||||
|
key: string;
|
||||||
|
value?: unknown;
|
||||||
|
dry_run?: boolean;
|
||||||
|
target?: Record<string, unknown>;
|
||||||
|
reason?: string | null;
|
||||||
|
}): Promise<ConfigurationChangeRequest> {
|
||||||
|
const response = await apiFetch<{ request: ConfigurationChangeRequest }>(settings, "/api/v1/admin/configuration-change-requests", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
return response.request;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function approveConfigurationChangeRequest(settings: ApiSettings, requestId: string, reason?: string | null): Promise<ConfigurationChangeRequest> {
|
||||||
|
const response = await apiFetch<{ request: ConfigurationChangeRequest }>(settings, `/api/v1/admin/configuration-change-requests/${encodeURIComponent(requestId)}/approve`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ reason: reason ?? null })
|
||||||
|
});
|
||||||
|
return response.request;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchConfigurationPackageCatalogValidation(settings: ApiSettings): Promise<{ validation: Record<string, unknown> }> {
|
||||||
|
return apiFetch(settings, "/api/v1/admin/configuration-packages/catalog");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dryRunConfigurationPackage(settings: ApiSettings, payload: ConfigurationPackageRunPayload): Promise<{
|
||||||
|
diagnostics: ConfigurationPackageDiagnostic[];
|
||||||
|
required_data: ConfigurationPackageRequiredData[];
|
||||||
|
plan: ConfigurationPackagePlanItem[];
|
||||||
|
}> {
|
||||||
|
return apiFetch(settings, "/api/v1/admin/configuration-packages/dry-run", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConfigurationPackage(settings: ApiSettings, payload: ConfigurationPackageRunPayload): Promise<{
|
||||||
|
diagnostics: ConfigurationPackageDiagnostic[];
|
||||||
|
created_refs: Record<string, string>;
|
||||||
|
updated_refs: Record<string, string>;
|
||||||
|
}> {
|
||||||
|
return apiFetch(settings, "/api/v1/admin/configuration-packages/apply", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportConfigurationPackage(settings: ApiSettings, payload: {
|
||||||
|
tenant_id?: string | null;
|
||||||
|
scopes?: string[];
|
||||||
|
module_ids?: string[];
|
||||||
|
object_refs?: string[];
|
||||||
|
}): Promise<{
|
||||||
|
fragments: ConfigurationPackageFragment[];
|
||||||
|
data_requirements: ConfigurationPackageRequiredData[];
|
||||||
|
diagnostics: ConfigurationPackageDiagnostic[];
|
||||||
|
}> {
|
||||||
|
return apiFetch(settings, "/api/v1/admin/configuration-packages/export", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Card } 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";
|
||||||
|
|
||||||
export default function AdminOverviewPanel({ settings, onSelect, availableSections }: { settings: ApiSettings; onSelect: (section: string) => void; availableSections: ReadonlySet<string> }) {
|
export default function AdminOverviewPanel({ settings, onSelect, availableSections }: {settings: ApiSettings;onSelect: (section: string) => void;availableSections: ReadonlySet<string>;}) {
|
||||||
const [overview, setOverview] = useState<AdminOverview | null>(null);
|
const [overview, setOverview] = useState<AdminOverview | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -13,81 +13,112 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
|
|||||||
async function load() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try { setOverview(await fetchAdminOverview(settings)); }
|
try {setOverview(await fetchAdminOverview(settings));}
|
||||||
catch (err) { setError(adminErrorMessage(err)); }
|
catch (err) {setError(adminErrorMessage(err));} finally
|
||||||
finally { setLoading(false); }
|
{setLoading(false);}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
|
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl]);
|
||||||
|
|
||||||
const hasSystemMetrics = Boolean(overview && [
|
const hasSystemMetrics = Boolean(overview && [
|
||||||
overview.tenant_count,
|
overview.tenant_count,
|
||||||
overview.system_account_count,
|
overview.system_account_count,
|
||||||
overview.system_group_template_count,
|
overview.system_group_template_count,
|
||||||
overview.system_role_template_count
|
overview.system_role_template_count].
|
||||||
].some((value) => value !== null && value !== undefined));
|
some((value) => value !== null && value !== undefined));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminPageLayout title="Administration" description="System-wide governance and tenant-local access management, separated by scope and enforced by the backend." loading={loading} error={error} actions={<Button onClick={() => void load()} disabled={loading}>Reload</Button>}>
|
<AdminPageLayout title="i18n:govoplan-admin.administration.b8be3d12" description="i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499" loading={loading} error={error} actions={<Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button>}>
|
||||||
{overview && <>
|
{overview && <>
|
||||||
{hasSystemMetrics && <>
|
{hasSystemMetrics && <>
|
||||||
<div className="admin-overview-section-label">System</div>
|
<div className="admin-overview-section-label">i18n:govoplan-admin.system.bc0792d8</div>
|
||||||
<div className="metric-grid">
|
<div className="metric-grid">
|
||||||
<Metric title="Tenants" value={overview.tenant_count ?? "—"} text="Registered tenant spaces." />
|
<Metric title="i18n:govoplan-admin.tenants.1f7ae776" value={overview.tenant_count ?? "—"} text="i18n:govoplan-admin.registered_tenant_spaces.61e17d70" />
|
||||||
<Metric title="Users" value={overview.system_account_count ?? "—"} text="Global login accounts across all tenants." />
|
<Metric title="i18n:govoplan-admin.users.57f2b181" value={overview.system_account_count ?? "—"} text="i18n:govoplan-admin.global_login_accounts_across_all_tenants.2aab129d" />
|
||||||
<Metric title="Central groups" value={overview.system_group_template_count ?? "—"} text="System-governed group definitions." />
|
<Metric title="i18n:govoplan-admin.central_groups.5c9b5b66" value={overview.system_group_template_count ?? "—"} text="i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9" />
|
||||||
<Metric title="Tenant roles" value={overview.system_role_template_count ?? "—"} text="Centrally governed tenant roles." />
|
<Metric title="i18n:govoplan-admin.tenant_roles.51aca82d" value={overview.system_role_template_count ?? "—"} text="i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b" />
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
{hasSystemArea(availableSections) && <Card title="System administration">
|
{hasAnySection(availableSections, platformSectionIds) && <Card title="ADMINISTRATION">
|
||||||
<div className="admin-overview-grid">
|
<div className="admin-overview-grid">
|
||||||
{availableSections.has("system-settings") && <AreaLink title="General" text="Instance defaults and tenant governance capabilities." onClick={() => onSelect("system-settings")} />}
|
{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-tenants") && <AreaLink title="Tenants" text="Create, suspend and govern tenant spaces." onClick={() => onSelect("system-tenants")} />}
|
{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-roles") && <AreaLink title="System roles" text="Instance-wide roles assigned directly to global accounts." onClick={() => onSelect("system-roles")} />}
|
{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-role-templates") && <AreaLink title="Tenant roles" text="Centrally governed tenant roles and their availability across tenants." onClick={() => onSelect("system-role-templates")} />}
|
{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-groups") && <AreaLink title="Groups" text="Group definitions made available or required in selected tenants." onClick={() => onSelect("system-groups")} />}
|
{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-users") && <AreaLink title="Users" text="Global accounts, tenant memberships and system-role assignments." onClick={() => onSelect("system-users")} />}
|
|
||||||
{availableSections.has("system-mail-servers") && <AreaLink title="Mail servers" text="Reusable encrypted SMTP/IMAP profiles and mail policy." onClick={() => onSelect("system-mail-servers")} />}
|
|
||||||
{availableSections.has("system-retention") && <AreaLink title="Retention" text="Instance privacy retention policy and lower-level override permissions." onClick={() => onSelect("system-retention")} />}
|
|
||||||
{availableSections.has("system-modules") && <AreaLink title="Modules" text="Installed modules, runtime state and startup state." onClick={() => onSelect("system-modules")} />}
|
|
||||||
{availableSections.has("system-audit") && <AreaLink title="Audit" text="System-level administrative history." onClick={() => onSelect("system-audit")} />}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>}
|
</Card>}
|
||||||
|
|
||||||
<div className="admin-overview-section-label">Active tenant: {overview.active_tenant_name}</div>
|
{hasAnySection(availableSections, globalSectionIds) && <Card title="GLOBAL">
|
||||||
|
<div className="admin-overview-grid">
|
||||||
|
{availableSections.has("system-tenants") && <AreaLink title="i18n:govoplan-admin.tenants.1f7ae776" text="i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39" onClick={() => onSelect("system-tenants")} />}
|
||||||
|
{availableSections.has("system-roles") && <AreaLink title="i18n:govoplan-admin.system_roles.a9461aa6" text="i18n:govoplan-admin.instance_wide_roles_assigned_directly_to_global_.91050488" onClick={() => onSelect("system-roles")} />}
|
||||||
|
{availableSections.has("system-role-templates") && <AreaLink title="i18n:govoplan-admin.tenant_roles.51aca82d" text="i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1" onClick={() => onSelect("system-role-templates")} />}
|
||||||
|
{availableSections.has("system-groups") && <AreaLink title="i18n:govoplan-admin.central_groups.5c9b5b66" text="i18n:govoplan-admin.group_definitions_made_available_or_required_in_.55402e16" onClick={() => onSelect("system-groups")} />}
|
||||||
|
{availableSections.has("system-users") && <AreaLink title="i18n:govoplan-admin.users.57f2b181" text="i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f" onClick={() => onSelect("system-users")} />}
|
||||||
|
{availableSections.has("system-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("system-file-connectors")} />}
|
||||||
|
{availableSections.has("system-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("system-mail-servers")} />}
|
||||||
|
{availableSections.has("system-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069" onClick={() => onSelect("system-retention")} />}
|
||||||
|
</div>
|
||||||
|
</Card>}
|
||||||
|
|
||||||
|
<div className="admin-overview-section-label">i18n:govoplan-admin.active_tenant.dfadf0aa {overview.active_tenant_name}</div>
|
||||||
<div className="metric-grid">
|
<div className="metric-grid">
|
||||||
<Metric title="Tenant users" value={`${overview.active_user_count}/${overview.user_count}`} text="Active and total memberships." />
|
<Metric title="i18n:govoplan-admin.tenant_users.cb800b38" value={`${overview.active_user_count}/${overview.user_count}`} text="i18n:govoplan-admin.active_and_total_memberships.c0c20f10" />
|
||||||
<Metric title="Groups" value={overview.group_count} text="Tenant-local and centrally managed groups." />
|
<Metric title="i18n:govoplan-admin.groups.ae9629f4" value={overview.group_count} text="i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5" />
|
||||||
<Metric title="Roles" value={overview.role_count} text="Tenant-local and centrally managed roles." />
|
<Metric title="i18n:govoplan-admin.roles.47dcc27d" value={overview.role_count} text="i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2" />
|
||||||
<Metric title="API keys" value={overview.active_api_key_count} text="Active tenant automation credentials." />
|
<Metric title="i18n:govoplan-admin.api_keys.94fcf3c2" value={overview.active_api_key_count} text="i18n:govoplan-admin.active_tenant_automation_credentials.240c659e" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card title="Tenant administration">
|
{hasAnySection(availableSections, tenantSectionIds) && <Card title="TENANT">
|
||||||
<div className="admin-overview-grid">
|
<div className="admin-overview-grid">
|
||||||
{availableSections.has("tenant-settings") && <AreaLink title="General" text="Tenant locale and tenant-specific settings." onClick={() => onSelect("tenant-settings")} />}
|
{availableSections.has("tenant-roles") && <AreaLink title="i18n:govoplan-admin.roles.47dcc27d" text="i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea" onClick={() => onSelect("tenant-roles")} />}
|
||||||
{availableSections.has("tenant-roles") && <AreaLink title="Roles" text="Tenant permission bundles and system-managed role copies." onClick={() => onSelect("tenant-roles")} />}
|
{availableSections.has("tenant-groups") && <AreaLink title="i18n:govoplan-admin.groups.ae9629f4" text="i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6" onClick={() => onSelect("tenant-groups")} />}
|
||||||
{availableSections.has("tenant-groups") && <AreaLink title="Groups" text="Tenant memberships and inherited roles." onClick={() => onSelect("tenant-groups")} />}
|
{availableSections.has("tenant-users") && <AreaLink title="i18n:govoplan-admin.users.57f2b181" text="i18n:govoplan-admin.membership_status_groups_and_direct_roles_in_the.ab6c10b6" onClick={() => onSelect("tenant-users")} />}
|
||||||
{availableSections.has("tenant-users") && <AreaLink title="Users" text="Membership status, groups and direct roles in the active tenant." onClick={() => onSelect("tenant-users")} />}
|
{availableSections.has("tenant-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("tenant-file-connectors")} />}
|
||||||
{availableSections.has("tenant-mail-servers") && <AreaLink title="Mail servers" text="Reusable encrypted SMTP/IMAP profiles and mail policy." onClick={() => onSelect("tenant-mail-servers")} />}
|
{availableSections.has("tenant-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("tenant-mail-servers")} />}
|
||||||
{availableSections.has("tenant-retention") && <AreaLink title="Retention" text="Tenant-level privacy retention limits inherited by owned objects." onClick={() => onSelect("tenant-retention")} />}
|
{availableSections.has("tenant-api-keys") && <AreaLink title="i18n:govoplan-admin.api_keys.94fcf3c2" text="i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54" onClick={() => onSelect("tenant-api-keys")} />}
|
||||||
{availableSections.has("tenant-api-keys") && <AreaLink title="API keys" text="Scoped automation credentials capped by owner permissions." onClick={() => onSelect("tenant-api-keys")} />}
|
{availableSections.has("tenant-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d" onClick={() => onSelect("tenant-retention")} />}
|
||||||
{availableSections.has("tenant-audit") && <AreaLink title="Audit" text="Tenant-level administrative history only." onClick={() => onSelect("tenant-audit")} />}
|
{availableSections.has("tenant-settings") && <AreaLink title="i18n:govoplan-admin.general.9239ee2c" text="i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b" onClick={() => onSelect("tenant-settings")} />}
|
||||||
|
{availableSections.has("tenant-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.tenant_level_administrative_history_only.55495c3c" onClick={() => onSelect("tenant-audit")} />}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>}
|
||||||
|
|
||||||
|
{hasAnySection(availableSections, groupSectionIds) && <Card title="GROUP">
|
||||||
|
<div className="admin-overview-grid">
|
||||||
|
{availableSections.has("tenant-group-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.group_file_connector_policy_limits" onClick={() => onSelect("tenant-group-file-connectors")} />}
|
||||||
|
{availableSections.has("tenant-group-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.group_mail_server_policy_limits" onClick={() => onSelect("tenant-group-mail-servers")} />}
|
||||||
|
{availableSections.has("tenant-group-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.group_retention_policy_limits" onClick={() => onSelect("tenant-group-retention")} />}
|
||||||
|
</div>
|
||||||
|
</Card>}
|
||||||
|
|
||||||
|
{hasAnySection(availableSections, userSectionIds) && <Card title="USER">
|
||||||
|
<div className="admin-overview-grid">
|
||||||
|
{availableSections.has("tenant-user-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.user_file_connector_policy_limits" onClick={() => onSelect("tenant-user-file-connectors")} />}
|
||||||
|
{availableSections.has("tenant-user-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.user_mail_server_policy_limits" onClick={() => onSelect("tenant-user-mail-servers")} />}
|
||||||
|
{availableSections.has("tenant-user-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.user_retention_policy_limits" onClick={() => onSelect("tenant-user-retention")} />}
|
||||||
|
</div>
|
||||||
|
</Card>}
|
||||||
</>}
|
</>}
|
||||||
</AdminPageLayout>
|
</AdminPageLayout>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSystemArea(sections: ReadonlySet<string>): boolean {
|
const platformSectionIds = ["system-modules", "system-configuration-packages", "system-settings", "system-configuration-changes", "system-audit"];
|
||||||
return ["system-settings", "system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-mail-servers", "system-retention", "system-modules", "system-audit"].some((section) => sections.has(section));
|
const globalSectionIds = ["system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-file-connectors", "system-mail-servers", "system-retention"];
|
||||||
|
const tenantSectionIds = ["tenant-roles", "tenant-groups", "tenant-users", "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", "tenant-retention", "tenant-settings", "tenant-audit"];
|
||||||
|
const groupSectionIds = ["tenant-group-file-connectors", "tenant-group-mail-servers", "tenant-group-retention"];
|
||||||
|
const userSectionIds = ["tenant-user-file-connectors", "tenant-user-mail-servers", "tenant-user-retention"];
|
||||||
|
|
||||||
|
function hasAnySection(sections: ReadonlySet<string>, candidates: readonly string[]): boolean {
|
||||||
|
return candidates.some((section) => sections.has(section));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 <Card title={title}><strong className="module-big-number">{value}</strong><p className="muted">{text}</p></Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AreaLink({ title, text, onClick }: { title: string; text: string; onClick: () => void }) {
|
function AreaLink({ title, text, onClick }: {title: string;text: string;onClick: () => void;}) {
|
||||||
return <button className="admin-overview-link" onClick={onClick}><strong>{title}</strong><span>{text}</span></button>;
|
return <button className="admin-overview-link" onClick={onClick}><strong>{title}</strong><span>{text}</span></button>;
|
||||||
}
|
}
|
||||||
|
|||||||
170
webui/src/features/admin/ConfigurationChangesPanel.tsx
Normal file
170
webui/src/features/admin/ConfigurationChangesPanel.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { Check, RefreshCw } from "lucide-react";
|
||||||
|
import type { ApiSettings } from "@govoplan/core-webui";
|
||||||
|
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks } from "@govoplan/core-webui";
|
||||||
|
import {
|
||||||
|
approveConfigurationChangeRequest,
|
||||||
|
fetchConfigurationChangesDelta,
|
||||||
|
type ConfigurationChangeRecord,
|
||||||
|
type ConfigurationChangeRequest } from
|
||||||
|
"../../api/admin";
|
||||||
|
|
||||||
|
const DELTA_KEY = "admin:configuration-changes";
|
||||||
|
|
||||||
|
export default function ConfigurationChangesPanel({ settings, canApprove }: {settings: ApiSettings;canApprove: boolean;}) {
|
||||||
|
const [requests, setRequests] = useState<ConfigurationChangeRequest[]>([]);
|
||||||
|
const [history, setHistory] = useState<ConfigurationChangeRecord[]>([]);
|
||||||
|
const requestsRef = useRef<ConfigurationChangeRequest[]>([]);
|
||||||
|
const historyRef = useRef<ConfigurationChangeRecord[]>([]);
|
||||||
|
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [busyId, setBusyId] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
let nextWatermark = getDeltaWatermark(DELTA_KEY);
|
||||||
|
let nextRequests = requestsRef.current;
|
||||||
|
let nextHistory = historyRef.current;
|
||||||
|
let hasMore = false;
|
||||||
|
do {
|
||||||
|
const payload = await fetchConfigurationChangesDelta(settings, { since: nextWatermark });
|
||||||
|
nextRequests = payload.full ? payload.requests : mergeDeltaRows(nextRequests, payload.requests, payload.deleted, (request) => request.id, { sort: sortConfigurationRequests });
|
||||||
|
nextHistory = payload.full ? payload.history : mergeDeltaRows(nextHistory, payload.history, payload.deleted, (record) => record.id, { sort: sortConfigurationHistory });
|
||||||
|
nextWatermark = payload.watermark ?? null;
|
||||||
|
hasMore = payload.has_more;
|
||||||
|
} while (hasMore);
|
||||||
|
requestsRef.current = nextRequests;
|
||||||
|
historyRef.current = nextHistory;
|
||||||
|
setRequests(nextRequests);
|
||||||
|
setHistory(nextHistory);
|
||||||
|
setDeltaWatermark(DELTA_KEY, nextWatermark);
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestsRef.current = [];
|
||||||
|
historyRef.current = [];
|
||||||
|
resetDeltaWatermark(DELTA_KEY);
|
||||||
|
void load();
|
||||||
|
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
|
||||||
|
|
||||||
|
async function approve(request: ConfigurationChangeRequest) {
|
||||||
|
setBusyId(request.id);
|
||||||
|
setError("");
|
||||||
|
setMessage("");
|
||||||
|
try {
|
||||||
|
await approveConfigurationChangeRequest(settings, request.id);
|
||||||
|
setMessage(i18nMessage("i18n:govoplan-admin.approved_value.52da808b", { value0: request.key }));
|
||||||
|
await load();
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setBusyId("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = useMemo(() => requests.filter((item) => item.status !== "applied" && item.status !== "rejected"), [requests]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminPageLayout
|
||||||
|
title="i18n:govoplan-admin.configuration_changes.82933bbb"
|
||||||
|
description="i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509"
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
success={message}
|
||||||
|
actions={<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}>
|
||||||
|
|
||||||
|
<Card title="i18n:govoplan-admin.requests.f7194e6a">
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>i18n:govoplan-admin.setting.fb449f71</th>
|
||||||
|
<th>i18n:govoplan-admin.status.bae7d5be</th>
|
||||||
|
<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 title="i18n:govoplan-admin.history.90ccd649">
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>i18n:govoplan-admin.version.2da600bf</th>
|
||||||
|
<th>i18n:govoplan-admin.setting.fb449f71</th>
|
||||||
|
<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>
|
||||||
|
</AdminPageLayout>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTone(status: string): string {
|
||||||
|
if (status === "approved" || status === "applied") return "success";
|
||||||
|
if (status === "pending_approval") return "warning";
|
||||||
|
if (status === "rejected") return "error";
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetLabel(target?: Record<string, unknown>): string {
|
||||||
|
if (!target || !Object.keys(target).length) return "-";
|
||||||
|
return Object.entries(target).map(([key, value]) => `${key}: ${String(value)}`).join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortConfigurationRequests(left: ConfigurationChangeRequest, right: ConfigurationChangeRequest): number {
|
||||||
|
return new Date(right.updated_at || right.requested_at).getTime() - new Date(left.updated_at || left.requested_at).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortConfigurationHistory(left: ConfigurationChangeRecord, right: ConfigurationChangeRecord): number {
|
||||||
|
return right.version - left.version;
|
||||||
|
}
|
||||||
374
webui/src/features/admin/ConfigurationPackagesPanel.tsx
Normal file
374
webui/src/features/admin/ConfigurationPackagesPanel.tsx
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Check, Download, Play, RefreshCw } from "lucide-react";
|
||||||
|
import type { ApiSettings } from "@govoplan/core-webui";
|
||||||
|
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, i18nMessage } from "@govoplan/core-webui";
|
||||||
|
import {
|
||||||
|
applyConfigurationPackage,
|
||||||
|
createConfigurationChangeRequest,
|
||||||
|
dryRunConfigurationPackage,
|
||||||
|
exportConfigurationPackage,
|
||||||
|
fetchConfigurationPackageCatalogValidation,
|
||||||
|
type ConfigurationChangeRequest,
|
||||||
|
type ConfigurationPackageDiagnostic,
|
||||||
|
type ConfigurationPackagePlanItem,
|
||||||
|
type ConfigurationPackageRequiredData } from
|
||||||
|
"../../api/admin";
|
||||||
|
|
||||||
|
const SAMPLE_ACCESS_PACKAGE = {
|
||||||
|
package_id: "govoplan.access.minimal-office",
|
||||||
|
name: "i18n:govoplan-admin.minimal_office_access.af48f49a",
|
||||||
|
version: "0.1.0",
|
||||||
|
required_modules: [{ module_id: "access" }],
|
||||||
|
required_capabilities: ["configuration.provider", "access.configuration"],
|
||||||
|
fragments: [
|
||||||
|
{
|
||||||
|
module_id: "access",
|
||||||
|
fragment_type: "roles",
|
||||||
|
payload: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
slug: "case-clerk",
|
||||||
|
name: "i18n:govoplan-admin.case_clerk.b78a314a",
|
||||||
|
description: "i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349",
|
||||||
|
permissions: ["admin:users:read"]
|
||||||
|
}]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module_id: "access",
|
||||||
|
fragment_type: "groups",
|
||||||
|
payload: { items: [{ slug: "front-office", name: "i18n:govoplan-admin.front_office.d9dcfee1" }] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module_id: "access",
|
||||||
|
fragment_type: "group_role_assignments",
|
||||||
|
payload: { items: [{ group: "front-office", role: "case-clerk" }] }
|
||||||
|
}]
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
type DryRunResult = {
|
||||||
|
diagnostics: ConfigurationPackageDiagnostic[];
|
||||||
|
required_data: ConfigurationPackageRequiredData[];
|
||||||
|
plan: ConfigurationPackagePlanItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApplyResult = {
|
||||||
|
diagnostics: ConfigurationPackageDiagnostic[];
|
||||||
|
created_refs: Record<string, string>;
|
||||||
|
updated_refs: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ConfigurationPackagesPanel({ settings, canWrite }: {settings: ApiSettings;canWrite: boolean;}) {
|
||||||
|
const [catalogValidation, setCatalogValidation] = useState<Record<string, unknown> | null>(null);
|
||||||
|
const [packageText, setPackageText] = useState(() => JSON.stringify(SAMPLE_ACCESS_PACKAGE, null, 2));
|
||||||
|
const [tenantId, setTenantId] = useState("");
|
||||||
|
const [suppliedDataText, setSuppliedDataText] = useState("{}");
|
||||||
|
const [changeRequestId, setChangeRequestId] = useState("");
|
||||||
|
const [dryRun, setDryRun] = useState<DryRunResult | null>(null);
|
||||||
|
const [applyResult, setApplyResult] = useState<ApplyResult | null>(null);
|
||||||
|
const [exportText, setExportText] = useState("");
|
||||||
|
const [lastRequest, setLastRequest] = useState<ConfigurationChangeRequest | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [busy, setBusy] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
const parsedPackage = useMemo(() => parseObject(packageText), [packageText]);
|
||||||
|
const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]);
|
||||||
|
const canRun = Boolean(parsedPackage.value && parsedSuppliedData.value);
|
||||||
|
|
||||||
|
async function loadCatalog() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const response = await fetchConfigurationPackageCatalogValidation(settings);
|
||||||
|
setCatalogValidation(response.validation);
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {void loadCatalog();}, [settings.accessToken, settings.apiBaseUrl]);
|
||||||
|
|
||||||
|
async function runDryRun() {
|
||||||
|
const manifest = requireParsedPackage();
|
||||||
|
const suppliedData = requireParsedSuppliedData();
|
||||||
|
if (!manifest || !suppliedData) return;
|
||||||
|
setBusy("dry-run");
|
||||||
|
setError("");
|
||||||
|
setMessage("");
|
||||||
|
setApplyResult(null);
|
||||||
|
try {
|
||||||
|
const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData));
|
||||||
|
setDryRun(result);
|
||||||
|
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b" : "i18n:govoplan-admin.preflight_passed.c0c99055");
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setBusy("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestApproval() {
|
||||||
|
const manifest = requireParsedPackage();
|
||||||
|
if (!manifest) return;
|
||||||
|
setBusy("approval");
|
||||||
|
setError("");
|
||||||
|
setMessage("");
|
||||||
|
try {
|
||||||
|
const request = await createConfigurationChangeRequest(settings, {
|
||||||
|
key: "configuration_packages.apply",
|
||||||
|
value: manifest,
|
||||||
|
dry_run: true,
|
||||||
|
target: tenantId.trim() ? { tenant_id: tenantId.trim() } : {},
|
||||||
|
reason: "i18n:govoplan-admin.configuration_package_apply.c270e5f3"
|
||||||
|
});
|
||||||
|
setLastRequest(request);
|
||||||
|
setChangeRequestId(request.id);
|
||||||
|
setMessage(i18nMessage("i18n:govoplan-admin.change_request_created_value.4af6d3d2", { value0: request.id }));
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setBusy("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPackage() {
|
||||||
|
const manifest = requireParsedPackage();
|
||||||
|
const suppliedData = requireParsedSuppliedData();
|
||||||
|
if (!manifest || !suppliedData) return;
|
||||||
|
setBusy("apply");
|
||||||
|
setError("");
|
||||||
|
setMessage("");
|
||||||
|
try {
|
||||||
|
const result = await applyConfigurationPackage(settings, runPayload(manifest, suppliedData, changeRequestId.trim() || null));
|
||||||
|
setApplyResult(result);
|
||||||
|
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.apply_finished_with_blockers.78487a12" : "i18n:govoplan-admin.package_applied.63782ce7");
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setBusy("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAccessPackage() {
|
||||||
|
setBusy("export");
|
||||||
|
setError("");
|
||||||
|
setMessage("");
|
||||||
|
try {
|
||||||
|
const result = await exportConfigurationPackage(settings, {
|
||||||
|
tenant_id: tenantId.trim() || null,
|
||||||
|
module_ids: ["access"],
|
||||||
|
scopes: tenantId.trim() ? ["tenant"] : ["system"]
|
||||||
|
});
|
||||||
|
const exported = {
|
||||||
|
package_id: tenantId.trim() ? "govoplan.export.access-tenant" : "govoplan.export.access-system",
|
||||||
|
name: tenantId.trim() ? "i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c" : "i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c",
|
||||||
|
version: "0.1.0",
|
||||||
|
required_modules: [{ module_id: "access" }],
|
||||||
|
required_capabilities: ["configuration.provider", "access.configuration"],
|
||||||
|
fragments: result.fragments,
|
||||||
|
data_requirements: result.data_requirements
|
||||||
|
};
|
||||||
|
setExportText(JSON.stringify(exported, null, 2));
|
||||||
|
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.export_finished_with_blockers.e2f70611" : "i18n:govoplan-admin.access_configuration_exported.098f200d");
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setBusy("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPayload(manifest: Record<string, unknown>, suppliedData: Record<string, unknown>, requestId?: string | null) {
|
||||||
|
return {
|
||||||
|
package: manifest,
|
||||||
|
tenant_id: tenantId.trim() || null,
|
||||||
|
supplied_data: suppliedData,
|
||||||
|
change_request_id: requestId ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireParsedPackage(): Record<string, unknown> | null {
|
||||||
|
if (!parsedPackage.value) {
|
||||||
|
setError(parsedPackage.error || "i18n:govoplan-admin.package_json_must_be_an_object.db825bb3");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsedPackage.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireParsedSuppliedData(): Record<string, unknown> | null {
|
||||||
|
if (!parsedSuppliedData.value) {
|
||||||
|
setError(parsedSuppliedData.error || "i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsedSuppliedData.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminPageLayout
|
||||||
|
title="i18n:govoplan-admin.configuration_packages.eb2f05f1"
|
||||||
|
description="i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929"
|
||||||
|
loading={loading}
|
||||||
|
error={error || parsedPackage.error || parsedSuppliedData.error || ""}
|
||||||
|
success={message}
|
||||||
|
actions={<Button onClick={() => void loadCatalog()} disabled={loading || Boolean(busy)}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}>
|
||||||
|
|
||||||
|
<Card title="i18n:govoplan-admin.catalog.4a88d27b">
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<StatusBadge status={catalogValidation?.valid ? "success" : catalogValidation?.configured ? "warning" : "inactive"} label={catalogValidation?.valid ? "i18n:govoplan-admin.valid.a4aefa35" : catalogValidation?.configured ? "i18n:govoplan-admin.needs_attention.a126722e" : "i18n:govoplan-admin.not_configured.811931bb"} />
|
||||||
|
{catalogValidation?.signed !== undefined && <StatusBadge status={catalogValidation.signed ? "success" : "inactive"} label={catalogValidation.signed ? "i18n:govoplan-admin.signed.6e3665d8" : "i18n:govoplan-admin.unsigned.e91344ea"} />}
|
||||||
|
{catalogValidation?.trusted !== undefined && <StatusBadge status={catalogValidation.trusted ? "success" : "warning"} label={catalogValidation.trusted ? "i18n:govoplan-admin.trusted.99f7ed54" : "i18n:govoplan-admin.untrusted.cdc7838a"} />}
|
||||||
|
</div>
|
||||||
|
<pre className="code-panel module-install-plan-commands">{JSON.stringify(catalogValidation ?? {}, null, 2)}</pre>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="i18n:govoplan-admin.package.7431e3df">
|
||||||
|
<div className="module-installer-request-grid">
|
||||||
|
<label className="wide"><span>i18n:govoplan-admin.tenant_id.59eba244</span><input value={tenantId} onChange={(event) => setTenantId(event.target.value)} placeholder="active tenant" /></label>
|
||||||
|
<label className="wide"><span>i18n:govoplan-admin.change_request_id.96ee3239</span><input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." /></label>
|
||||||
|
<label className="wide"><span>i18n:govoplan-admin.package_json.a2b10f38</span><textarea rows={18} value={packageText} onChange={(event) => setPackageText(event.target.value)} /></label>
|
||||||
|
<label className="wide"><span>i18n:govoplan-admin.supplied_data_json.6932bfb7</span><textarea rows={6} value={suppliedDataText} onChange={(event) => setSuppliedDataText(event.target.value)} /></label>
|
||||||
|
</div>
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<Button onClick={() => void runDryRun()} disabled={!canRun || Boolean(busy)}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button>
|
||||||
|
<Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button>
|
||||||
|
<Button variant="primary" onClick={() => void applyPackage()} disabled={!canWrite || !canRun || Boolean(busy)}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
|
||||||
|
<Button onClick={() => void exportAccessPackage()} disabled={Boolean(busy)}><Download size={16} /> i18n:govoplan-admin.export_access.9a2eb91e</Button>
|
||||||
|
</div>
|
||||||
|
{lastRequest && <p className="muted small-note">i18n:govoplan-admin.last_request.4508ef35 {lastRequest.id} ({lastRequest.status})</p>}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{dryRun && <Card title="i18n:govoplan-admin.preflight.8016a487">
|
||||||
|
<PackageDiagnostics diagnostics={dryRun.diagnostics} />
|
||||||
|
<RequiredData items={dryRun.required_data} />
|
||||||
|
<PackagePlan items={dryRun.plan} />
|
||||||
|
</Card>}
|
||||||
|
|
||||||
|
{applyResult && <Card title="i18n:govoplan-admin.apply_result.0fde1c3c">
|
||||||
|
<PackageDiagnostics diagnostics={applyResult.diagnostics} />
|
||||||
|
<ReferenceMap title="i18n:govoplan-admin.created.accf40c8" refs={applyResult.created_refs} />
|
||||||
|
<ReferenceMap title="i18n:govoplan-admin.updated.f2f8570d" refs={applyResult.updated_refs} />
|
||||||
|
</Card>}
|
||||||
|
|
||||||
|
{exportText && <Card title="i18n:govoplan-admin.export.f3e4fadb">
|
||||||
|
<textarea className="code-panel module-install-plan-commands" rows={16} value={exportText} onChange={(event) => setExportText(event.target.value)} />
|
||||||
|
</Card>}
|
||||||
|
</AdminPageLayout>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseObject(text: string): {value: Record<string, unknown> | null;error: string;} {
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(text) as unknown;
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return { value: null, error: "i18n:govoplan-admin.json_must_be_an_object.848569c9" };
|
||||||
|
return { value: value as Record<string, unknown>, error: "" };
|
||||||
|
} catch (err) {
|
||||||
|
return { value: null, error: err instanceof Error ? err.message : "i18n:govoplan-admin.invalid_json.01ccb74f" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) {
|
||||||
|
if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>;
|
||||||
|
return (
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table">
|
||||||
|
<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>
|
||||||
|
<tbody>
|
||||||
|
{diagnostics.map((item, 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[];}) {
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>i18n:govoplan-admin.required_data.1b1c1b34</h3>
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<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[];}) {
|
||||||
|
if (items.length === 0) return <p className="muted">i18n:govoplan-admin.no_plan_items.7108c582</p>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>i18n:govoplan-admin.plan.ae2f98a0</h3>
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<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>;}) {
|
||||||
|
const entries = Object.entries(refs);
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table">
|
||||||
|
<tbody>
|
||||||
|
{entries.map(([key, value]) => <tr key={key}><td><code>{key}</code></td><td>{value}</td></tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnosticTone(severity: ConfigurationPackageDiagnostic["severity"]): string {
|
||||||
|
if (severity === "blocker") return "error";
|
||||||
|
if (severity === "warning") return "warning";
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
|
||||||
|
function planTone(action: ConfigurationPackagePlanItem["action"]): string {
|
||||||
|
if (action === "blocked") return "error";
|
||||||
|
if (action === "skip" || action === "noop") return "inactive";
|
||||||
|
if (action === "update" || action === "bind") return "warning";
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
@@ -17,9 +17,9 @@ import {
|
|||||||
type GovernanceAssignment,
|
type GovernanceAssignment,
|
||||||
type GovernanceTemplateItem,
|
type GovernanceTemplateItem,
|
||||||
type PermissionItem,
|
type PermissionItem,
|
||||||
type TenantAdminItem
|
type TenantAdminItem } from
|
||||||
} from "../../api/admin";
|
"../../api/admin";
|
||||||
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels } from "@govoplan/core-webui";
|
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||||
|
|
||||||
const emptyDraft = {
|
const emptyDraft = {
|
||||||
slug: "",
|
slug: "",
|
||||||
@@ -35,12 +35,12 @@ export default function GovernanceTemplatesPanel({
|
|||||||
kind,
|
kind,
|
||||||
canWrite,
|
canWrite,
|
||||||
onAuthRefresh
|
onAuthRefresh
|
||||||
}: {
|
|
||||||
settings: ApiSettings;
|
|
||||||
kind: "group" | "role";
|
|
||||||
canWrite: boolean;
|
|
||||||
onAuthRefresh: () => Promise<void>;
|
|
||||||
}) {
|
}: {settings: ApiSettings;kind: "group" | "role";canWrite: boolean;onAuthRefresh: () => Promise<void>;}) {
|
||||||
const [items, setItems] = useState<GovernanceTemplateItem[]>([]);
|
const [items, setItems] = useState<GovernanceTemplateItem[]>([]);
|
||||||
const [tenants, setTenants] = useState<TenantAdminItem[]>([]);
|
const [tenants, setTenants] = useState<TenantAdminItem[]>([]);
|
||||||
const [permissions, setPermissions] = useState<PermissionItem[]>([]);
|
const [permissions, setPermissions] = useState<PermissionItem[]>([]);
|
||||||
@@ -48,46 +48,63 @@ export default function GovernanceTemplatesPanel({
|
|||||||
const [viewing, setViewing] = useState<GovernanceTemplateItem | null>(null);
|
const [viewing, setViewing] = useState<GovernanceTemplateItem | null>(null);
|
||||||
const [deleting, setDeleting] = useState<GovernanceTemplateItem | null>(null);
|
const [deleting, setDeleting] = useState<GovernanceTemplateItem | null>(null);
|
||||||
const [draft, setDraft] = useState(emptyDraft);
|
const [draft, setDraft] = useState(emptyDraft);
|
||||||
|
const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft));
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
|
const dirty = editing !== null && draftKey(draft) !== savedDraftKey;
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
onSave: save,
|
||||||
|
onDiscard: closeEditor
|
||||||
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const [nextItems, nextTenants, nextPermissions] = await Promise.all([
|
const [nextItems, nextTenants, nextPermissions] = await Promise.all([
|
||||||
fetchGovernanceTemplates(settings, kind),
|
fetchGovernanceTemplates(settings, kind),
|
||||||
fetchTenants(settings),
|
fetchTenants(settings),
|
||||||
kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])
|
kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])]
|
||||||
]);
|
);
|
||||||
setItems(nextItems);
|
setItems(nextItems);
|
||||||
setTenants(nextTenants);
|
setTenants(nextTenants);
|
||||||
setPermissions(nextPermissions.filter((item) => item.level === "tenant"));
|
setPermissions(nextPermissions.filter((item) => item.level === "tenant"));
|
||||||
} catch (err) { setError(adminErrorMessage(err)); }
|
} catch (err) {setError(adminErrorMessage(err));} finally
|
||||||
finally { setLoading(false); }
|
{setLoading(false);}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl, kind]);
|
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl, kind]);
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
setDraft(emptyDraft);
|
setDraft(emptyDraft);
|
||||||
|
setSavedDraftKey(draftKey(emptyDraft));
|
||||||
setEditing("new");
|
setEditing("new");
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(item: GovernanceTemplateItem) {
|
function openEdit(item: GovernanceTemplateItem) {
|
||||||
setDraft({
|
const nextDraft = {
|
||||||
slug: item.slug,
|
slug: item.slug,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
description: item.description || "",
|
description: item.description || "",
|
||||||
isActive: item.is_active,
|
isActive: item.is_active,
|
||||||
permissions: item.permissions,
|
permissions: item.permissions,
|
||||||
assignments: item.assignments
|
assignments: item.assignments
|
||||||
});
|
};
|
||||||
|
setDraft(nextDraft);
|
||||||
|
setSavedDraftKey(draftKey(nextDraft));
|
||||||
setEditing(item);
|
setEditing(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeEditor() {
|
||||||
|
setEditing(null);
|
||||||
|
setDraft(emptyDraft);
|
||||||
|
setSavedDraftKey(draftKey(emptyDraft));
|
||||||
|
}
|
||||||
|
|
||||||
function assignmentMode(tenantId: string): "none" | "available" | "required" {
|
function assignmentMode(tenantId: string): "none" | "available" | "required" {
|
||||||
return draft.assignments.find((item) => item.tenant_id === tenantId)?.mode ?? "none";
|
return draft.assignments.find((item) => item.tenant_id === tenantId)?.mode ?? "none";
|
||||||
}
|
}
|
||||||
@@ -95,13 +112,13 @@ export default function GovernanceTemplatesPanel({
|
|||||||
function setAssignment(tenantId: string, mode: "none" | "available" | "required") {
|
function setAssignment(tenantId: string, mode: "none" | "available" | "required") {
|
||||||
setDraft((current) => ({
|
setDraft((current) => ({
|
||||||
...current,
|
...current,
|
||||||
assignments: mode === "none"
|
assignments: mode === "none" ?
|
||||||
? current.assignments.filter((item) => item.tenant_id !== tenantId)
|
current.assignments.filter((item) => item.tenant_id !== tenantId) :
|
||||||
: [...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }]
|
[...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }]
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save(): Promise<boolean> {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +132,7 @@ export default function GovernanceTemplatesPanel({
|
|||||||
is_active: draft.isActive,
|
is_active: draft.isActive,
|
||||||
assignments: draft.assignments
|
assignments: draft.assignments
|
||||||
});
|
});
|
||||||
setSuccess(`${kind === "group" ? "Group" : "Role"} template created.`);
|
setSuccess(i18nMessage("i18n:govoplan-admin.value_template_created.d68a5166", { value0: kind === "group" ? "i18n:govoplan-admin.group.171a0606" : "i18n:govoplan-admin.role.c3f104d1" }));
|
||||||
} else if (editing) {
|
} else if (editing) {
|
||||||
await updateGovernanceTemplate(settings, editing.id, {
|
await updateGovernanceTemplate(settings, editing.id, {
|
||||||
name: draft.name,
|
name: draft.name,
|
||||||
@@ -124,13 +141,14 @@ export default function GovernanceTemplatesPanel({
|
|||||||
is_active: draft.isActive,
|
is_active: draft.isActive,
|
||||||
assignments: draft.assignments
|
assignments: draft.assignments
|
||||||
});
|
});
|
||||||
setSuccess(`${draft.name} updated and synchronized to assigned tenants.`);
|
setSuccess(i18nMessage("i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2", { value0: draft.name }));
|
||||||
}
|
}
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
await load();
|
await load();
|
||||||
await onAuthRefresh();
|
await onAuthRefresh();
|
||||||
} catch (err) { setError(adminErrorMessage(err)); }
|
return true;
|
||||||
finally { setBusy(false); }
|
} catch (err) {setError(adminErrorMessage(err));return false;} finally
|
||||||
|
{setBusy(false);}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove() {
|
async function remove() {
|
||||||
@@ -139,47 +157,47 @@ export default function GovernanceTemplatesPanel({
|
|||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
await deleteGovernanceTemplate(settings, deleting.id);
|
await deleteGovernanceTemplate(settings, deleting.id);
|
||||||
setSuccess(`${deleting.name} deleted.`);
|
setSuccess(i18nMessage("i18n:govoplan-admin.value_deleted.3c4bf574", { value0: deleting.name }));
|
||||||
setDeleting(null);
|
setDeleting(null);
|
||||||
await load();
|
await load();
|
||||||
await onAuthRefresh();
|
await onAuthRefresh();
|
||||||
} catch (err) { setError(adminErrorMessage(err)); }
|
} catch (err) {setError(adminErrorMessage(err));} finally
|
||||||
finally { setBusy(false); }
|
{setBusy(false);}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = useMemo<DataGridColumn<GovernanceTemplateItem>[]>(() => [
|
const columns = useMemo<DataGridColumn<GovernanceTemplateItem>[]>(() => [
|
||||||
{
|
{
|
||||||
id: "template", header: kind === "group" ? "Group template" : "Tenant role", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true,
|
id: "template", header: kind === "group" ? "i18n:govoplan-admin.group_template.973e0fa6" : "i18n:govoplan-admin.tenant_role.6b53115d", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true,
|
||||||
value: (row) => `${row.name} ${row.slug}`,
|
value: (row) => `${row.name} ${row.slug}`,
|
||||||
render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div>
|
render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "tenants", header: "Tenant availability", width: 320, minWidth: 210, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true,
|
id: "tenants", header: "i18n:govoplan-admin.tenant_availability.067a4f31", width: 320, minWidth: 210, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true,
|
||||||
value: (row) => row.assignments.map((assignment) => tenants.find((tenant) => tenant.id === assignment.tenant_id)?.name || assignment.tenant_id).join(", ") || "—",
|
value: (row) => row.assignments.map((assignment) => tenants.find((tenant) => tenant.id === assignment.tenant_id)?.name || assignment.tenant_id).join(", ") || "—",
|
||||||
render: (row) => row.assignments.length ? row.assignments.map((assignment) => {
|
render: (row) => row.assignments.length ? row.assignments.map((assignment) => {
|
||||||
const tenant = tenants.find((item) => item.id === assignment.tenant_id);
|
const tenant = tenants.find((item) => item.id === assignment.tenant_id);
|
||||||
return `${tenant?.name || assignment.tenant_id} (${assignment.mode})`;
|
return i18nMessage("i18n:govoplan-admin.value_value.c189e8bc", { value0: tenant?.name || assignment.tenant_id, value1: assignment.mode });
|
||||||
}).join(", ") : "—"
|
}).join(", ") : "—"
|
||||||
},
|
},
|
||||||
...(kind === "role" ? [{
|
...(kind === "role" ? [{
|
||||||
id: "permissions", header: "Permissions", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const,
|
id: "permissions", header: "i18n:govoplan-admin.permissions.d06d5557", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const,
|
||||||
value: (row: GovernanceTemplateItem) => row.effective_permission_count
|
value: (row: GovernanceTemplateItem) => row.effective_permission_count
|
||||||
}] : []),
|
}] : []),
|
||||||
{ id: "status", header: "Status", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
|
{ id: "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: "Actions", 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) => <div className="admin-icon-actions">
|
||||||
<AdminIconButton label={`Inspect ${row.name}`} icon={<Search />} onClick={() => setViewing(row)} />
|
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.inspect_value.9d5d1071", { value0: row.name })} icon={<Search />} onClick={() => setViewing(row)} />
|
||||||
<AdminIconButton label={`Edit ${row.name}`} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canWrite} />
|
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.edit_value.fad75899", { value0: row.name })} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canWrite} />
|
||||||
<AdminIconButton label={`Delete ${row.name}`} icon={<Trash2 />} variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} />
|
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.delete_value.4d18989e", { value0: row.name })} icon={<Trash2 />} variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}],
|
||||||
], [canWrite, kind, tenants]);
|
[canWrite, kind, tenants]);
|
||||||
|
|
||||||
const title = kind === "group" ? "Central groups" : "Tenant roles";
|
const title = kind === "group" ? "i18n:govoplan-admin.central_groups.5c9b5b66" : "i18n:govoplan-admin.tenant_roles.51aca82d";
|
||||||
const description = kind === "group"
|
const description = kind === "group" ?
|
||||||
? "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local."
|
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9" :
|
||||||
: "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.";
|
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -189,41 +207,45 @@ export default function GovernanceTemplatesPanel({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
success={success}
|
success={success}
|
||||||
actions={<><Button onClick={() => void load()} disabled={loading}>Reload</Button><AdminIconButton label={kind === "group" ? "Add group template" : "Add tenant role"} icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canWrite} /></>}
|
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button><AdminIconButton label={kind === "group" ? "i18n:govoplan-admin.add_group_template.b74d8f0f" : "i18n:govoplan-admin.add_tenant_role.fcd904ea"} icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canWrite} /></>}>
|
||||||
>
|
|
||||||
<div className="admin-table-surface">
|
<div className="admin-table-surface">
|
||||||
<DataGrid id={`admin-system-${kind}-templates-v3`} rows={items} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText={`No central ${kind} templates found.`} />
|
<DataGrid id={`admin-system-${kind}-templates-v3`} rows={items} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText={i18nMessage("i18n:govoplan-admin.no_central_value_templates_found.081149fa", { value0: kind })} />
|
||||||
</div>
|
</div>
|
||||||
</AdminPageLayout>
|
</AdminPageLayout>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={editing !== null}
|
open={editing !== null}
|
||||||
title={editing === "new" ? (kind === "group" ? "Create group template" : "Create tenant role") : (kind === "group" ? "Edit group template" : "Edit tenant role")}
|
title={editing === "new" ? kind === "group" ? "i18n:govoplan-admin.create_group_template.72407248" : "i18n:govoplan-admin.create_tenant_role.f58db104" : kind === "group" ? "i18n:govoplan-admin.edit_group_template.9bc72d21" : "i18n:govoplan-admin.edit_tenant_role.c15a260d"}
|
||||||
onClose={() => !busy && setEditing(null)}
|
onClose={() => !busy && setEditing(null)}
|
||||||
className="admin-dialog admin-dialog-wide"
|
className="admin-dialog admin-dialog-wide"
|
||||||
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void save()} disabled={busy || !draft.name.trim() || !draft.slug.trim()}>{busy ? "Saving…" : kind === "group" ? "Save template" : "Save tenant role"}</Button></>}
|
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>i18n:govoplan-admin.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabled={busy || !draft.name.trim() || !draft.slug.trim()}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : kind === "group" ? "i18n:govoplan-admin.save_template.0885fab2" : "i18n:govoplan-admin.save_tenant_role.8fc0d37d"}</Button></>}>
|
||||||
>
|
|
||||||
<div className="admin-form-grid two-columns">
|
<div className="admin-form-grid two-columns">
|
||||||
<FormField label="Name"><input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-admin.name.709a2322"><input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||||
<FormField label="Slug"><input value={draft.slug} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-admin.slug.094da9b9"><input value={draft.slug} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
|
||||||
<FormField label="Status"><select value={draft.isActive ? "active" : "inactive"} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">Active</option><option value="inactive">Inactive</option></select></FormField>
|
<FormField label="i18n:govoplan-admin.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-admin.active.a733b809</option><option value="inactive">i18n:govoplan-admin.inactive.09af574c</option></select></FormField>
|
||||||
<FormField label="Description"><textarea rows={3} value={draft.description} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-admin.description.55f8ebc8"><textarea rows={3} value={draft.description} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||||
</div>
|
</div>
|
||||||
{kind === "role" && <div className="form-field"><span className="form-label">Tenant permissions</span><AdminSelectionList options={permissions.map((permission) => ({ id: permission.scope, label: permission.label, description: permission.description }))} selected={draft.permissions} onChange={(next) => setDraft({ ...draft, permissions: next })} /></div>}
|
{kind === "role" && <div className="form-field"><span className="form-label">i18n:govoplan-admin.tenant_permissions.246294bc</span><AdminSelectionList options={permissions.map((permission) => ({ id: permission.scope, label: permission.label, description: permission.description }))} selected={draft.permissions} onChange={(next) => setDraft({ ...draft, permissions: next })} /></div>}
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
<span className="form-label">Tenant availability</span>
|
<span className="form-label">i18n:govoplan-admin.tenant_availability.067a4f31</span>
|
||||||
<div className="admin-selection-list admin-governance-mode">
|
<div className="admin-selection-list admin-governance-mode">
|
||||||
{tenants.map((tenant) => <div className="admin-tenant-assignment-row" key={tenant.id}><span><strong>{tenant.name}</strong><small>{tenant.slug}</small></span><select value={assignmentMode(tenant.id)} onChange={(event) => setAssignment(tenant.id, event.target.value as "none" | "available" | "required")}><option value="none">Not available</option><option value="available">Available</option><option value="required">Required</option></select></div>)}
|
{tenants.map((tenant) => <div className="admin-tenant-assignment-row" key={tenant.id}><span><strong>{tenant.name}</strong><small>{tenant.slug}</small></span><select value={assignmentMode(tenant.id)} onChange={(event) => setAssignment(tenant.id, event.target.value as "none" | "available" | "required")}><option value="none">i18n:govoplan-admin.not_available.d1a17af1</option><option value="available">i18n:govoplan-admin.available.7c62a142</option><option value="required">i18n:govoplan-admin.required.eed6bfb4</option></select></div>)}
|
||||||
</div>
|
</div>
|
||||||
<p className="muted small-note">Required means the definition must remain present and system-controlled. It does not automatically assign users or grant permissions.</p>
|
<p className="muted small-note">i18n:govoplan-admin.required_means_the_definition_must_remain_presen.8462063c</p>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={Boolean(viewing)} title={viewing?.name || "Template details"} onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>Close</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>Kind</dt><dd>{viewing.kind}</dd></div><div><dt>Slug</dt><dd>{viewing.slug}</dd></div><div><dt>Status</dt><dd>{viewing.is_active ? "Active" : "Inactive"}</dd></div><div><dt>Tenants</dt><dd>{viewing.assignments.length || "None"}</dd></div><div><dt>Description</dt><dd>{viewing.description || "—"}</dd></div><div><dt>Permissions</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 ? joinLabels(viewing.permissions.map((name) => ({ name }))) : "—"}</dd></div></dl>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={Boolean(deleting)} title={kind === "group" ? "Delete group template" : "Delete tenant role"} message={`Delete ${deleting?.name}? Removal is blocked while a materialized tenant definition still has members or assignments.`} confirmLabel={kind === "group" ? "Delete template" : "Delete tenant role"} 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()} />
|
||||||
</>
|
</>);
|
||||||
);
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function draftKey(draft: typeof emptyDraft): string {
|
||||||
|
return JSON.stringify(draft);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
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 } from "@govoplan/core-webui";
|
import { AdminPageLayout, adminErrorMessage, Button, dispatchPlatformModulesChanged, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
cancelModuleInstallerRequest,
|
cancelModuleInstallerRequest,
|
||||||
clearModuleInstallPlan,
|
clearModuleInstallPlan,
|
||||||
|
createConfigurationChangeRequest,
|
||||||
createModuleInstallerRequest,
|
createModuleInstallerRequest,
|
||||||
fetchModuleCatalog,
|
fetchModuleCatalog,
|
||||||
fetchModuleInstallPlan,
|
fetchModuleInstallPlan,
|
||||||
fetchModuleInstallerRequests,
|
fetchModuleInstallerRequests,
|
||||||
fetchModuleInstallerRuns,
|
fetchModuleInstallerRuns,
|
||||||
fetchModulePackageCatalog,
|
fetchModulePackageCatalog,
|
||||||
|
fetchSystemSettings,
|
||||||
planModuleInstallFromCatalog,
|
planModuleInstallFromCatalog,
|
||||||
planModuleUninstall,
|
planModuleUninstall,
|
||||||
retryModuleInstallerRequest,
|
retryModuleInstallerRequest,
|
||||||
updateModuleInstallPlan,
|
updateModuleInstallPlan,
|
||||||
updateModuleState,
|
updateModuleState,
|
||||||
|
updateSystemSettings,
|
||||||
|
type ConfigurationChangeRequest,
|
||||||
type ModuleCatalogItem,
|
type ModuleCatalogItem,
|
||||||
type ModuleCatalogResponse,
|
type ModuleCatalogResponse,
|
||||||
type ModuleInstallerRequestListResponse,
|
type ModuleInstallerRequestListResponse,
|
||||||
@@ -22,21 +26,24 @@ import {
|
|||||||
type ModuleInstallerRunListResponse,
|
type ModuleInstallerRunListResponse,
|
||||||
type ModuleInstallPlanItem,
|
type ModuleInstallPlanItem,
|
||||||
type ModuleInstallPlanResponse,
|
type ModuleInstallPlanResponse,
|
||||||
|
type ModuleLicenseDiagnostics,
|
||||||
type ModulePackageCatalogResponse,
|
type ModulePackageCatalogResponse,
|
||||||
type ModulePackageCatalogItem
|
type ModulePackageCatalogItem } from
|
||||||
} from "../../api/admin";
|
"../../api/admin";
|
||||||
|
|
||||||
export default function ModuleManagementPanel({ settings, canWrite, canAccessMaintenance }: { settings: ApiSettings; canWrite: boolean; canAccessMaintenance: boolean }) {
|
export default function ModuleManagementPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) {
|
||||||
const [catalog, setCatalog] = useState<ModuleCatalogResponse | null>(null);
|
const [catalog, setCatalog] = useState<ModuleCatalogResponse | null>(null);
|
||||||
const [installPlan, setInstallPlan] = useState<ModuleInstallPlanResponse | null>(null);
|
const [installPlan, setInstallPlan] = useState<ModuleInstallPlanResponse | null>(null);
|
||||||
const [installerRuns, setInstallerRuns] = useState<ModuleInstallerRunListResponse | null>(null);
|
const [installerRuns, setInstallerRuns] = useState<ModuleInstallerRunListResponse | null>(null);
|
||||||
const [installerRequests, setInstallerRequests] = useState<ModuleInstallerRequestListResponse | null>(null);
|
const [installerRequests, setInstallerRequests] = useState<ModuleInstallerRequestListResponse | null>(null);
|
||||||
const [packageCatalog, setPackageCatalog] = useState<ModulePackageCatalogResponse | null>(null);
|
const [packageCatalog, setPackageCatalog] = useState<ModulePackageCatalogResponse | null>(null);
|
||||||
const [draftEnabled, setDraftEnabled] = useState<Set<string> | null>(null);
|
const [draftEnabled, setDraftEnabled] = useState<Set<string> | null>(null);
|
||||||
|
const [moduleStateRequest, setModuleStateRequest] = useState<ModuleStateRequestState | null>(null);
|
||||||
const [draftPlanItems, setDraftPlanItems] = useState<ModuleInstallPlanItem[]>([]);
|
const [draftPlanItems, setDraftPlanItems] = useState<ModuleInstallPlanItem[]>([]);
|
||||||
const [requestOptions, setRequestOptions] = useState<InstallerRequestFormOptions>(() => defaultInstallerRequestOptions());
|
const [requestOptions, setRequestOptions] = useState<InstallerRequestFormOptions>(() => defaultInstallerRequestOptions());
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [maintenanceBusy, setMaintenanceBusy] = useState(false);
|
||||||
const [planBusy, setPlanBusy] = useState(false);
|
const [planBusy, setPlanBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
@@ -46,12 +53,12 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const [loaded, loadedPlan, loadedRuns, loadedRequests, loadedPackageCatalog] = await Promise.all([
|
const [loaded, loadedPlan, loadedRuns, loadedRequests, loadedPackageCatalog] = await Promise.all([
|
||||||
fetchModuleCatalog(settings),
|
fetchModuleCatalog(settings),
|
||||||
fetchModuleInstallPlan(settings),
|
fetchModuleInstallPlan(settings),
|
||||||
fetchModuleInstallerRuns(settings),
|
fetchModuleInstallerRuns(settings),
|
||||||
fetchModuleInstallerRequests(settings),
|
fetchModuleInstallerRequests(settings),
|
||||||
fetchModulePackageCatalog(settings)
|
fetchModulePackageCatalog(settings)]
|
||||||
]);
|
);
|
||||||
setCatalog(loaded);
|
setCatalog(loaded);
|
||||||
setInstallPlan(loadedPlan);
|
setInstallPlan(loadedPlan);
|
||||||
setInstallerRuns(loadedRuns);
|
setInstallerRuns(loadedRuns);
|
||||||
@@ -66,7 +73,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
|
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl]);
|
||||||
|
|
||||||
const desired = draftEnabled ?? new Set(catalog?.desired_enabled ?? []);
|
const desired = draftEnabled ?? new Set(catalog?.desired_enabled ?? []);
|
||||||
const dirty = Boolean(catalog && !sameSet(desired, new Set(catalog.desired_enabled)));
|
const dirty = Boolean(catalog && !sameSet(desired, new Set(catalog.desired_enabled)));
|
||||||
@@ -76,37 +83,100 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
const maintenanceEnabled = Boolean(catalog?.maintenance_mode.enabled || installPlan?.maintenance_mode.enabled);
|
const maintenanceEnabled = Boolean(catalog?.maintenance_mode.enabled || installPlan?.maintenance_mode.enabled);
|
||||||
const planDirty = Boolean(installPlan && JSON.stringify(normalizePlanItems(draftPlanItems)) !== JSON.stringify(normalizePlanItems(installPlan.items)));
|
const planDirty = Boolean(installPlan && JSON.stringify(normalizePlanItems(draftPlanItems)) !== JSON.stringify(normalizePlanItems(installPlan.items)));
|
||||||
const planValid = planValidationError(draftPlanItems) === "";
|
const planValid = planValidationError(draftPlanItems) === "";
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: dirty || planDirty,
|
||||||
|
onSave: saveDirtyChanges,
|
||||||
|
onDiscard: () => {
|
||||||
|
if (catalog) setDraftEnabled(new Set(catalog.desired_enabled));
|
||||||
|
if (installPlan) setDraftPlanItems(installPlan.items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const saveLabel = maintenanceEnabled ?
|
||||||
|
moduleStateRequest ? "i18n:govoplan-admin.apply_approved_request.ab218d9a" : "i18n:govoplan-admin.create_request_and_apply.da79e153" :
|
||||||
|
moduleStateRequest ? "i18n:govoplan-admin.waiting_for_maintenance.60d00019" : "i18n:govoplan-admin.create_dry_run_request.05b9cbdb";
|
||||||
|
const saveDisabled = !canWrite || !dirty || busy || !maintenanceEnabled && Boolean(moduleStateRequest);
|
||||||
|
|
||||||
function setModuleDesired(module: ModuleCatalogItem, enabled: boolean) {
|
function setModuleDesired(module: ModuleCatalogItem, enabled: boolean) {
|
||||||
if (module.protected) return;
|
if (module.protected) return;
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
|
setModuleStateRequest(null);
|
||||||
setDraftEnabled((current) => {
|
setDraftEnabled((current) => {
|
||||||
const next = new Set(current ?? catalog?.desired_enabled ?? []);
|
const next = new Set(current ?? catalog?.desired_enabled ?? []);
|
||||||
if (enabled) next.add(module.id);
|
if (enabled) next.add(module.id);else
|
||||||
else next.delete(module.id);
|
next.delete(module.id);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save(): Promise<boolean> {
|
||||||
if (!draftEnabled) return;
|
if (!draftEnabled) return false;
|
||||||
|
const enabledModules = Array.from(draftEnabled).sort();
|
||||||
|
const payloadKey = moduleStatePayloadKey(enabledModules);
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
try {
|
try {
|
||||||
const updated = await updateModuleState(settings, Array.from(draftEnabled).sort());
|
let request = moduleStateRequest?.payloadKey === payloadKey ? moduleStateRequest.request : null;
|
||||||
|
if (!request) {
|
||||||
|
request = await createConfigurationChangeRequest(settings, {
|
||||||
|
key: "module_management.desired_enabled",
|
||||||
|
value: { enabled_modules: enabledModules },
|
||||||
|
dry_run: true,
|
||||||
|
target: { scope: "system" },
|
||||||
|
reason: "i18n:govoplan-admin.update_enabled_modules.9b7ae790"
|
||||||
|
});
|
||||||
|
setModuleStateRequest({ request, payloadKey });
|
||||||
|
}
|
||||||
|
if (!maintenanceEnabled) {
|
||||||
|
setSuccess(i18nMessage("i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07", { value0: request.id }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const updated = await updateModuleState(settings, enabledModules, request.id);
|
||||||
setCatalog(updated);
|
setCatalog(updated);
|
||||||
setDraftEnabled(new Set(updated.desired_enabled));
|
setDraftEnabled(new Set(updated.desired_enabled));
|
||||||
setSuccess(updated.notes[0] ?? "Module state saved.");
|
setModuleStateRequest(null);
|
||||||
|
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_state_saved.e3e22105");
|
||||||
dispatchPlatformModulesChanged();
|
dispatchPlatformModulesChanged();
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePlan() {
|
async function enableMaintenanceMode() {
|
||||||
|
setMaintenanceBusy(true);
|
||||||
|
setError("");
|
||||||
|
setSuccess("");
|
||||||
|
try {
|
||||||
|
const current = await fetchSystemSettings(settings);
|
||||||
|
const updated = await updateSystemSettings(settings, {
|
||||||
|
default_locale: current.default_locale,
|
||||||
|
allow_tenant_custom_groups: current.allow_tenant_custom_groups,
|
||||||
|
allow_tenant_custom_roles: current.allow_tenant_custom_roles,
|
||||||
|
allow_tenant_api_keys: current.allow_tenant_api_keys,
|
||||||
|
maintenance_mode: {
|
||||||
|
enabled: true,
|
||||||
|
message: current.maintenance_mode.message || "i18n:govoplan-admin.module_maintenance_in_progress.f6395152"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setCatalog((value) => value ? { ...value, maintenance_mode: updated.maintenance_mode } : value);
|
||||||
|
setInstallPlan((value) => value ? {
|
||||||
|
...value,
|
||||||
|
maintenance_mode: updated.maintenance_mode,
|
||||||
|
preflight: value.preflight ? { ...value.preflight, maintenance_mode: updated.maintenance_mode.enabled } : value.preflight
|
||||||
|
} : value);
|
||||||
|
setSuccess("i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c");
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setMaintenanceBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePlan(): Promise<boolean> {
|
||||||
setPlanBusy(true);
|
setPlanBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
@@ -114,14 +184,23 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
const updated = await updateModuleInstallPlan(settings, normalizePlanItems(draftPlanItems));
|
const updated = await updateModuleInstallPlan(settings, normalizePlanItems(draftPlanItems));
|
||||||
setInstallPlan(updated);
|
setInstallPlan(updated);
|
||||||
setDraftPlanItems(updated.items);
|
setDraftPlanItems(updated.items);
|
||||||
setSuccess(updated.notes[0] ?? "Module install plan saved.");
|
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_install_plan_saved.5d43d944");
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setPlanBusy(false);
|
setPlanBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveDirtyChanges(): Promise<boolean> {
|
||||||
|
let ok = true;
|
||||||
|
if (dirty) ok = await save() && ok;
|
||||||
|
if (planDirty) ok = await savePlan() && ok;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
async function clearPlan() {
|
async function clearPlan() {
|
||||||
setPlanBusy(true);
|
setPlanBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
@@ -130,7 +209,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
const updated = await clearModuleInstallPlan(settings);
|
const updated = await clearModuleInstallPlan(settings);
|
||||||
setInstallPlan(updated);
|
setInstallPlan(updated);
|
||||||
setDraftPlanItems([]);
|
setDraftPlanItems([]);
|
||||||
setSuccess(updated.notes[0] ?? "Module install plan cleared.");
|
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_install_plan_cleared.afa2698d");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -155,7 +234,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
const updated = await planModuleInstallFromCatalog(settings, item.module_id);
|
const updated = await planModuleInstallFromCatalog(settings, item.module_id);
|
||||||
setInstallPlan(updated);
|
setInstallPlan(updated);
|
||||||
setDraftPlanItems(updated.items);
|
setDraftPlanItems(updated.items);
|
||||||
setSuccess(updated.notes[0] ?? "Catalog install entry planned.");
|
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -171,7 +250,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
const updated = await planModuleUninstall(settings, module.id);
|
const updated = await planModuleUninstall(settings, module.id);
|
||||||
setInstallPlan(updated);
|
setInstallPlan(updated);
|
||||||
setDraftPlanItems(updated.items);
|
setDraftPlanItems(updated.items);
|
||||||
setSuccess(updated.notes[0] ?? "Package uninstall planned.");
|
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.package_uninstall_planned.56e8566d");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -186,11 +265,11 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
setSuccess("");
|
setSuccess("");
|
||||||
try {
|
try {
|
||||||
const request = await createModuleInstallerRequest(settings, normalizeRequestOptions(requestOptions, installPlan));
|
const request = await createModuleInstallerRequest(settings, normalizeRequestOptions(requestOptions, installPlan));
|
||||||
setSuccess(`Installer request queued: ${request.request_id}`);
|
setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_queued_value.00726abb", { value0: request.request_id }));
|
||||||
const [loadedRuns, loadedRequests] = await Promise.all([
|
const [loadedRuns, loadedRequests] = await Promise.all([
|
||||||
fetchModuleInstallerRuns(settings),
|
fetchModuleInstallerRuns(settings),
|
||||||
fetchModuleInstallerRequests(settings)
|
fetchModuleInstallerRequests(settings)]
|
||||||
]);
|
);
|
||||||
setInstallerRuns(loadedRuns);
|
setInstallerRuns(loadedRuns);
|
||||||
setInstallerRequests(loadedRequests);
|
setInstallerRequests(loadedRequests);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -206,7 +285,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
setSuccess("");
|
setSuccess("");
|
||||||
try {
|
try {
|
||||||
const request = await cancelModuleInstallerRequest(settings, requestId);
|
const request = await cancelModuleInstallerRequest(settings, requestId);
|
||||||
setSuccess(`Installer request cancelled: ${request.request_id}`);
|
setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d", { value0: request.request_id }));
|
||||||
setInstallerRequests(await fetchModuleInstallerRequests(settings));
|
setInstallerRequests(await fetchModuleInstallerRequests(settings));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
@@ -221,7 +300,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
setSuccess("");
|
setSuccess("");
|
||||||
try {
|
try {
|
||||||
const request = await retryModuleInstallerRequest(settings, requestId);
|
const request = await retryModuleInstallerRequest(settings, requestId);
|
||||||
setSuccess(`Installer request queued: ${request.request_id}`);
|
setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_queued_value.00726abb", { value0: request.request_id }));
|
||||||
setInstallerRequests(await fetchModuleInstallerRequests(settings));
|
setInstallerRequests(await fetchModuleInstallerRequests(settings));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
@@ -232,22 +311,33 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminPageLayout
|
<AdminPageLayout
|
||||||
title="System modules"
|
title="i18n:govoplan-admin.system_modules.629539f2"
|
||||||
description="Installed modules, active runtime state, and saved startup state."
|
description="i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
success={success}
|
success={success}
|
||||||
actions={<><Button onClick={() => void load()} disabled={loading || busy}>Reload</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || !dirty || busy}>{busy ? "Applying..." : "Save and apply"}</Button></>}
|
actions={<><Button onClick={() => void load()} disabled={loading || busy}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={saveDisabled}>{busy ? "i18n:govoplan-admin.working.049ac820" : saveLabel}</Button></>}>
|
||||||
>
|
|
||||||
{catalog && <>
|
{catalog && <>
|
||||||
<div className="metric-grid module-management-metrics">
|
<div className="metric-grid module-management-metrics">
|
||||||
<ModuleMetric label="Installed" value={catalog.modules.length} detail="Discovered packages" />
|
<ModuleMetric label="i18n:govoplan-admin.installed.7bb4405c" value={catalog.modules.length} detail="i18n:govoplan-admin.discovered_packages.660902de" />
|
||||||
<ModuleMetric label="Active" value={activeCount} detail="Running registry" />
|
<ModuleMetric label="i18n:govoplan-admin.active.a733b809" value={activeCount} detail="i18n:govoplan-admin.running_registry.5274b24b" />
|
||||||
<ModuleMetric label="Saved" value={desiredCount} detail="Startup state" />
|
<ModuleMetric label="i18n:govoplan-admin.saved.c0ae8f6e" value={desiredCount} detail="i18n:govoplan-admin.startup_state.d1300be6" />
|
||||||
<ModuleMetric label="Drift" value={pendingCount} detail={pendingCount ? "Runtime differs" : "Runtime matches"} tone={pendingCount ? "warning" : "good"} />
|
<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"} />
|
||||||
<ModuleMetric label="Maintenance" value={maintenanceEnabled ? "On" : "Off"} detail={canAccessMaintenance ? "Bypass allowed" : "Bypass denied"} tone={maintenanceEnabled ? "warning" : "info"} />
|
<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"} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{dirty && <div className="module-management-notes">
|
||||||
|
<p className="alert warning">i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed</p>
|
||||||
|
{moduleStateRequest && <p>i18n:govoplan-admin.change_request_ready.b17e8c57 <code>{moduleStateRequest.request.id}</code></p>}
|
||||||
|
{!maintenanceEnabled && <div className="button-row compact-actions">
|
||||||
|
<Button onClick={() => void enableMaintenanceMode()} disabled={!canWrite || !canAccessMaintenance || maintenanceBusy}>
|
||||||
|
{maintenanceBusy ? "i18n:govoplan-admin.enabling.2b8e03e6" : "i18n:govoplan-admin.enable_maintenance_mode.75f98d57"}
|
||||||
|
</Button>
|
||||||
|
</div>}
|
||||||
|
{!maintenanceEnabled && !canAccessMaintenance && <p className="alert warning">i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968</p>}
|
||||||
|
</div>}
|
||||||
|
|
||||||
<div className="module-management-list">
|
<div className="module-management-list">
|
||||||
{catalog.modules.map((module) => {
|
{catalog.modules.map((module) => {
|
||||||
const desiredEnabled = desired.has(module.id);
|
const desiredEnabled = desired.has(module.id);
|
||||||
@@ -261,27 +351,27 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
</div>
|
</div>
|
||||||
<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="Locked" />}
|
{module.protected && <StatusBadge status="locked" label="i18n:govoplan-admin.locked.a798882f" />}
|
||||||
{module.frontend_package && <span>{module.frontend_package}</span>}
|
{module.frontend_package && <span>{module.frontend_package}</span>}
|
||||||
{module.migration_module_id && <span>DB: {module.migration_module_id}</span>}
|
{module.migration_module_id && <span>i18n:govoplan-admin.db.e355c23a {module.migration_module_id}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="module-management-details">
|
<div className="module-management-details">
|
||||||
<span>Requires: {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span>
|
<span>i18n:govoplan-admin.requires.a4fc9357 {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span>
|
||||||
<span>Optional: {module.optional_dependencies.length ? module.optional_dependencies.join(", ") : "none"}</span>
|
<span>i18n:govoplan-admin.optional.166b8369 {module.optional_dependencies.length ? module.optional_dependencies.join(", ") : "none"}</span>
|
||||||
<span>Dependents: {module.dependents.length ? module.dependents.join(", ") : "none"}</span>
|
<span>i18n:govoplan-admin.dependents.072665c4 {module.dependents.length ? module.dependents.join(", ") : "none"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="module-management-toggle">
|
<div className="module-management-toggle">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label={desiredEnabled ? "Enabled" : "Disabled"}
|
label={desiredEnabled ? "i18n:govoplan-admin.enabled.df174a3f" : "i18n:govoplan-admin.disabled.f4f4473d"}
|
||||||
checked={desiredEnabled}
|
checked={desiredEnabled}
|
||||||
disabled={!canWrite || module.protected || busy}
|
disabled={!canWrite || module.protected || busy}
|
||||||
onChange={(checked) => setModuleDesired(module, checked)}
|
onChange={(checked) => setModuleDesired(module, checked)} />
|
||||||
/>
|
|
||||||
{module.install_uninstall_supported && <Button onClick={() => void addUninstallPlan(module)} disabled={!canWrite || planBusy || module.current_enabled || module.desired_enabled}>Plan uninstall</Button>}
|
{module.install_uninstall_supported && <Button onClick={() => void addUninstallPlan(module)} disabled={!canWrite || planBusy || module.current_enabled || module.desired_enabled}>i18n:govoplan-admin.plan_uninstall.e62804ab</Button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>);
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -292,27 +382,28 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
{packageCatalog && <div className="module-package-catalog">
|
{packageCatalog && <div className="module-package-catalog">
|
||||||
<div className="module-install-plan-heading">
|
<div className="module-install-plan-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>Package catalog</h2>
|
<h2>i18n:govoplan-admin.package_catalog.b36e6d31</h2>
|
||||||
<p>Approved package references that can be added to the operator install plan.</p>
|
<p>i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
{packageCatalog.channel && <StatusBadge status="inactive" label={`Channel: ${packageCatalog.channel}`} />}
|
{packageCatalog.channel && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.channel_value.d3894efe", { value0: packageCatalog.channel })} />}
|
||||||
{packageCatalog.sequence !== null && packageCatalog.sequence !== undefined && <StatusBadge status="inactive" label={`Seq ${packageCatalog.sequence}`} />}
|
{packageCatalog.sequence !== null && packageCatalog.sequence !== undefined && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.seq_value.0ae5fa47", { value0: packageCatalog.sequence })} />}
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={packageCatalog.trusted ? "success" : packageCatalog.signed ? "warning" : "inactive"}
|
status={packageCatalog.trusted ? "success" : packageCatalog.signed ? "warning" : "inactive"}
|
||||||
label={packageCatalog.trusted ? "Signed and trusted" : packageCatalog.signed ? "Signed, untrusted" : "Unsigned"}
|
label={packageCatalog.trusted ? "i18n:govoplan-admin.signed_and_trusted.bee31302" : packageCatalog.signed ? "i18n:govoplan-admin.signed_untrusted.864ed50c" : "i18n:govoplan-admin.unsigned.e91344ea"} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(packageCatalog.source || packageCatalog.path) && <p className="module-package-catalog-description">Catalog {packageCatalog.source_type ?? "source"}: <code>{packageCatalog.source ?? packageCatalog.path}</code></p>}
|
{(packageCatalog.source || packageCatalog.path) && <p className="module-package-catalog-description">i18n:govoplan-admin.catalog.4a88d27b {packageCatalog.source_type ?? "source"}: <code>{packageCatalog.source ?? packageCatalog.path}</code>{packageCatalog.cache_used && packageCatalog.cache_path ? <> i18n:govoplan-admin.cached_from.d73c5b2a <code>{packageCatalog.cache_path}</code></> : null}</p>}
|
||||||
{(packageCatalog.generated_at || packageCatalog.expires_at) && <p className="module-package-catalog-description">Generated: {packageCatalog.generated_at ?? "unknown"} · Expires: {packageCatalog.expires_at ?? "unknown"}</p>}
|
{(packageCatalog.generated_at || packageCatalog.not_before || packageCatalog.expires_at) && <p className="module-package-catalog-description">i18n:govoplan-admin.generated.a2edf57c {packageCatalog.generated_at ?? "unknown"} i18n:govoplan-admin.valid_after.c615c873 {packageCatalog.not_before ?? "now"} i18n:govoplan-admin.expires.2d21b7de {packageCatalog.expires_at ?? "unknown"}</p>}
|
||||||
|
<LicenseStatus license={packageCatalog.license} />
|
||||||
{packageCatalog.error && <p className="alert warning">{packageCatalog.error}</p>}
|
{packageCatalog.error && <p className="alert warning">{packageCatalog.error}</p>}
|
||||||
{packageCatalog.warnings.map((warning) => <p key={warning} className="alert warning">{warning}</p>)}
|
{packageCatalog.warnings.map((warning) => <p key={warning} className="alert warning">{warning}</p>)}
|
||||||
{!packageCatalog.configured && <div className="module-install-plan-empty">No package catalog configured. Set GOVOPLAN_MODULE_PACKAGE_CATALOG or GOVOPLAN_MODULE_PACKAGE_CATALOG_URL to enable curated install entries.</div>}
|
{!packageCatalog.configured && <div className="module-install-plan-empty">i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3</div>}
|
||||||
{packageCatalog.configured && packageCatalog.modules.length === 0 && !packageCatalog.error && <div className="module-install-plan-empty">Package catalog is configured but contains no entries.</div>}
|
{packageCatalog.configured && packageCatalog.modules.length === 0 && !packageCatalog.error && <div className="module-install-plan-empty">i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb</div>}
|
||||||
{packageCatalog.modules.length > 0 && <div className="module-package-catalog-list">
|
{packageCatalog.modules.length > 0 && <div className="module-package-catalog-list">
|
||||||
{packageCatalog.modules.map((item) => (
|
{packageCatalog.modules.map((item) =>
|
||||||
<div key={`${item.module_id}-${item.version ?? "latest"}`} className="module-package-catalog-row">
|
<div key={`${item.module_id}-${item.version ?? "latest"}`} className="module-package-catalog-row">
|
||||||
<div className="module-management-main">
|
<div className="module-management-main">
|
||||||
<div className="module-management-title">
|
<div className="module-management-title">
|
||||||
<strong>{item.name}</strong>
|
<strong>{item.name}</strong>
|
||||||
@@ -323,75 +414,75 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
{item.python_package && <span>{item.python_package}</span>}
|
{item.python_package && <span>{item.python_package}</span>}
|
||||||
{item.webui_package && <span>{item.webui_package}</span>}
|
{item.webui_package && <span>{item.webui_package}</span>}
|
||||||
{item.tags.length > 0 && <span>{item.tags.join(", ")}</span>}
|
{item.tags.length > 0 && <span>{item.tags.join(", ")}</span>}
|
||||||
{item.license_features.length > 0 && <span>License: {item.license_features.join(", ")}</span>}
|
{item.license_features.length > 0 && <span>i18n:govoplan-admin.license.de13bf1a {item.license_features.join(", ")}</span>}
|
||||||
{item.license_missing_features.length > 0 && <span>Missing: {item.license_missing_features.join(", ")}</span>}
|
{item.license_missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {item.license_missing_features.join(", ")}</span>}
|
||||||
</div>
|
</div>
|
||||||
{item.description && <p className="module-package-catalog-description">{item.description}</p>}
|
{item.description && <p className="module-package-catalog-description">{item.description}</p>}
|
||||||
{item.license_reason && <p className={`module-package-catalog-description${item.license_allowed ? "" : " alert warning"}`}>{item.license_reason}</p>}
|
{item.license_reason && <p className={`module-package-catalog-description${item.license_allowed ? "" : " alert warning"}`}>{item.license_reason}</p>}
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => void addCatalogItem(item)} disabled={!canWrite || planBusy || !packageCatalog.valid || !item.license_allowed || item.action !== "install"}>Plan install</Button>
|
<Button onClick={() => void addCatalogItem(item)} disabled={!canWrite || planBusy || !packageCatalog.valid || !item.license_allowed || item.action !== "install"}>i18n:govoplan-admin.plan_install.e82bffe6</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{installPlan && <div className="module-install-plan-panel">
|
{installPlan && <div className="module-install-plan-panel">
|
||||||
<div className="module-install-plan-heading">
|
<div className="module-install-plan-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>Operator install plan</h2>
|
<h2>i18n:govoplan-admin.operator_install_plan.b203aabc</h2>
|
||||||
<p>Plan package installs and removals here, then apply the rendered commands from an operator shell during maintenance mode.</p>
|
<p>i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<Button onClick={addPlanItem} disabled={!canWrite || planBusy}>Add plan item</Button>
|
<Button onClick={addPlanItem} disabled={!canWrite || planBusy}>i18n:govoplan-admin.add_plan_item.fa55f028</Button>
|
||||||
<Button onClick={() => void clearPlan()} disabled={!canWrite || planBusy || draftPlanItems.length === 0}>Clear</Button>
|
<Button onClick={() => void clearPlan()} disabled={!canWrite || planBusy || draftPlanItems.length === 0}>i18n:govoplan-admin.clear.719ea396</Button>
|
||||||
<Button variant="primary" onClick={() => void savePlan()} disabled={!canWrite || planBusy || !planDirty || !planValid}>{planBusy ? "Saving..." : "Save plan"}</Button>
|
<Button variant="primary" onClick={() => void savePlan()} disabled={!canWrite || planBusy || !planDirty || !planValid}>{planBusy ? "i18n:govoplan-admin.saving.ae7e8875" : "i18n:govoplan-admin.save_plan.842dc280"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{planValidationError(draftPlanItems) && <p className="alert warning">{planValidationError(draftPlanItems)}</p>}
|
{planValidationError(draftPlanItems) && <p className="alert warning">{planValidationError(draftPlanItems)}</p>}
|
||||||
{!maintenanceEnabled && <p className="alert warning">Maintenance mode is off. Package changes should be applied only after enabling maintenance mode.</p>}
|
{!maintenanceEnabled && <p className="alert warning">i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629</p>}
|
||||||
|
|
||||||
{installPlan.preflight && <div className={`module-install-preflight ${installPlan.preflight.allowed ? "is-allowed" : "is-blocked"}`}>
|
{installPlan.preflight && <div className={`module-install-preflight ${installPlan.preflight.allowed ? "is-allowed" : "is-blocked"}`}>
|
||||||
<div className="module-install-preflight-title">
|
<div className="module-install-preflight-title">
|
||||||
<strong>{installPlan.preflight.allowed ? "Installer preflight passed" : "Installer preflight blocked"}</strong>
|
<strong>{installPlan.preflight.allowed ? "i18n:govoplan-admin.installer_preflight_passed.6d5f8060" : "i18n:govoplan-admin.installer_preflight_blocked.cf83bf79"}</strong>
|
||||||
<span>{installPlan.preflight.restart_required ? "Restart/reload required after package changes" : "No package restart pending"}</span>
|
<span>{installPlan.preflight.restart_required ? "i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63" : "i18n:govoplan-admin.no_package_restart_pending.efe6b8ce"}</span>
|
||||||
{installPlan.preflight.frontend_rebuild_required && <span>WebUI rebuild required</span>}
|
{installPlan.preflight.frontend_rebuild_required && <span>i18n:govoplan-admin.webui_rebuild_required.010ce49f</span>}
|
||||||
</div>
|
</div>
|
||||||
{installPlan.preflight.issues.length > 0 && <div className="module-install-preflight-issues">
|
{installPlan.preflight.issues.length > 0 && <div className="module-install-preflight-issues">
|
||||||
{installPlan.preflight.issues.map((issue, index) => (
|
{installPlan.preflight.issues.map((issue, index) =>
|
||||||
<div key={`${issue.code}-${issue.module_id ?? "global"}-${index}`} className={`module-install-preflight-issue severity-${issue.severity}`}>
|
<div key={`${issue.code}-${issue.module_id ?? "global"}-${index}`} className={`module-install-preflight-issue severity-${issue.severity}`}>
|
||||||
<strong>{issue.severity}</strong>
|
<strong>{issue.severity}</strong>
|
||||||
<span>{issue.module_id ? `${issue.module_id}: ` : ""}{issue.message}</span>
|
<span>{issue.module_id ? `${issue.module_id}: ` : ""}{issue.message}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
{installPlan.preflight.checklist.length > 0 && <div className="module-install-checklist">
|
{installPlan.preflight.checklist.length > 0 && <div className="module-install-checklist">
|
||||||
{installPlan.preflight.checklist.map((item) => (
|
{installPlan.preflight.checklist.map((item) =>
|
||||||
<div key={item.id} className={`module-install-checklist-item status-${item.status}`}>
|
<div key={item.id} className={`module-install-checklist-item status-${item.status}`}>
|
||||||
<strong>{item.label}</strong>
|
<strong>{item.label}</strong>
|
||||||
<span>{item.status}</span>
|
<span>{item.status}</span>
|
||||||
{item.detail && <p>{item.detail}</p>}
|
{item.detail && <p>{item.detail}</p>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<div className="module-install-plan-list">
|
<div className="module-install-plan-list">
|
||||||
{draftPlanItems.length === 0 && <div className="module-install-plan-empty">No package changes planned.</div>}
|
{draftPlanItems.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_package_changes_planned.f12d8b33</div>}
|
||||||
{draftPlanItems.map((item, index) => (
|
{draftPlanItems.map((item, index) =>
|
||||||
<div className="module-install-plan-row" key={`${item.module_id}-${index}`}>
|
<div className="module-install-plan-row" key={`${item.module_id}-${index}`}>
|
||||||
<label><span>Action</span><select value={item.action} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { action: event.target.value as ModuleInstallPlanItem["action"] })}><option value="install">Install</option><option value="uninstall">Uninstall</option></select></label>
|
<label><span>i18n:govoplan-admin.action.97c89a4d</span><select value={item.action} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { action: event.target.value as ModuleInstallPlanItem["action"] })}><option value="install">i18n:govoplan-admin.install.fd6c3ebf</option><option value="uninstall">i18n:govoplan-admin.uninstall.a735da1d</option></select></label>
|
||||||
<label><span>Module</span><input value={item.module_id} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { module_id: event.target.value })} placeholder="files" /></label>
|
<label><span>i18n:govoplan-admin.module.b8ff0289</span><input value={item.module_id} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { module_id: event.target.value })} placeholder="files" /></label>
|
||||||
<label><span>Python package</span><input value={item.python_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { python_package: event.target.value })} placeholder="govoplan-files" /></label>
|
<label><span>i18n:govoplan-admin.python_package.34591c71</span><input value={item.python_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { python_package: event.target.value })} placeholder="govoplan-files" /></label>
|
||||||
<ToggleSwitch label="Destroy data" checked={Boolean(item.destroy_data)} onChange={(checked) => updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} />
|
<ToggleSwitch label="i18n:govoplan-admin.destroy_data.34557c3c" checked={Boolean(item.destroy_data)} onChange={(checked) => updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} />
|
||||||
<label className="wide"><span>Python ref</span><input value={item.python_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { python_ref: event.target.value })} placeholder="govoplan-files @ git+ssh://...@v0.1.4" /></label>
|
<label className="wide"><span>i18n:govoplan-admin.python_ref.1af35d0d</span><input value={item.python_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { python_ref: event.target.value })} placeholder="govoplan-files @ git+ssh://...@v0.1.4" /></label>
|
||||||
<label><span>WebUI package</span><input value={item.webui_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { webui_package: event.target.value })} placeholder="@govoplan/files-webui" /></label>
|
<label><span>i18n:govoplan-admin.webui_package.19645536</span><input value={item.webui_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { webui_package: event.target.value })} placeholder="@govoplan/files-webui" /></label>
|
||||||
<label className="wide"><span>WebUI ref</span><input value={item.webui_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { webui_ref: event.target.value })} placeholder="git+ssh://...#v0.1.4" /></label>
|
<label className="wide"><span>i18n:govoplan-admin.webui_ref.cbae3559</span><input value={item.webui_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { webui_ref: event.target.value })} placeholder="git+ssh://...#v0.1.4" /></label>
|
||||||
<label><span>Status</span><select value={item.status} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { status: event.target.value as ModuleInstallPlanItem["status"] })}><option value="planned">Planned</option><option value="applied">Applied</option><option value="blocked">Blocked</option></select></label>
|
<label><span>i18n:govoplan-admin.status.bae7d5be</span><select value={item.status} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { status: event.target.value as ModuleInstallPlanItem["status"] })}><option value="planned">i18n:govoplan-admin.planned.9cbe42aa</option><option value="applied">i18n:govoplan-admin.applied.a3e4a569</option><option value="blocked">i18n:govoplan-admin.blocked.99613c74</option></select></label>
|
||||||
<label className="wide"><span>Notes</span><input value={item.notes ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { notes: event.target.value })} /></label>
|
<label className="wide"><span>i18n:govoplan-admin.notes.70440046</span><input value={item.notes ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { notes: event.target.value })} /></label>
|
||||||
<div className="module-install-plan-actions"><Button onClick={() => setDraftPlanItems((items) => items.filter((_, itemIndex) => itemIndex !== index))} disabled={!canWrite || planBusy}>Remove</Button></div>
|
<div className="module-install-plan-actions"><Button onClick={() => setDraftPlanItems((items) => items.filter((_, itemIndex) => itemIndex !== index))} disabled={!canWrite || planBusy}>i18n:govoplan-admin.remove.e963907d</Button></div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre className="code-panel module-install-plan-commands">{installerCommand(installPlan.preflight?.frontend_rebuild_required ?? false)}</pre>
|
<pre className="code-panel module-install-plan-commands">{installerCommand(installPlan.preflight?.frontend_rebuild_required ?? false)}</pre>
|
||||||
@@ -401,26 +492,26 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
<div className="module-installer-request-panel">
|
<div className="module-installer-request-panel">
|
||||||
<div className="module-install-plan-heading">
|
<div className="module-install-plan-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>Daemon execution</h2>
|
<h2>i18n:govoplan-admin.daemon_execution.cc0fad8d</h2>
|
||||||
<p>Queue the saved plan for a separate installer daemon process.</p>
|
<p>i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="primary" onClick={() => void queueInstallerRequest()} disabled={!canWrite || !canAccessMaintenance || planBusy || planDirty || !maintenanceEnabled || !installPlan.preflight?.allowed}>
|
<Button variant="primary" onClick={() => void queueInstallerRequest()} disabled={!canWrite || !canAccessMaintenance || planBusy || planDirty || !maintenanceEnabled || !installPlan.preflight?.allowed}>
|
||||||
Queue supervised run
|
i18n:govoplan-admin.queue_supervised_run.a53f248c
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{planDirty && <p className="alert warning">Save the install plan before queueing a daemon request.</p>}
|
{planDirty && <p className="alert warning">i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691</p>}
|
||||||
{!maintenanceEnabled && <p className="alert warning">Maintenance mode must be enabled before queueing a daemon request.</p>}
|
{!maintenanceEnabled && <p className="alert warning">i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f</p>}
|
||||||
{!canAccessMaintenance && <p className="alert warning">Queueing installer requests requires maintenance access.</p>}
|
{!canAccessMaintenance && <p className="alert warning">i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac</p>}
|
||||||
<div className="module-installer-request-grid">
|
<div className="module-installer-request-grid">
|
||||||
<ToggleSwitch label="Run migrations" checked={requestOptions.migrateDatabase} onChange={(checked) => setRequestOptions((current) => ({ ...current, migrateDatabase: checked }))} disabled={!canWrite || planBusy} />
|
<ToggleSwitch label="i18n:govoplan-admin.run_migrations.db6e0ce2" checked={requestOptions.migrateDatabase} onChange={(checked) => setRequestOptions((current) => ({ ...current, migrateDatabase: checked }))} disabled={!canWrite || planBusy} />
|
||||||
<ToggleSwitch label="Build WebUI" checked={requestOptions.buildWebui} onChange={(checked) => setRequestOptions((current) => ({ ...current, buildWebui: checked }))} disabled={!canWrite || planBusy} />
|
<ToggleSwitch label="i18n:govoplan-admin.build_webui.fe8ccad7" checked={requestOptions.buildWebui} onChange={(checked) => setRequestOptions((current) => ({ ...current, buildWebui: checked }))} disabled={!canWrite || planBusy} />
|
||||||
<ToggleSwitch label="Activate installs" checked={requestOptions.activateInstalledModules} onChange={(checked) => setRequestOptions((current) => ({ ...current, activateInstalledModules: checked }))} disabled={!canWrite || planBusy} />
|
<ToggleSwitch label="i18n:govoplan-admin.activate_installs.731d22a1" checked={requestOptions.activateInstalledModules} onChange={(checked) => setRequestOptions((current) => ({ ...current, activateInstalledModules: checked }))} disabled={!canWrite || planBusy} />
|
||||||
<ToggleSwitch label="Drop uninstalls from startup" checked={requestOptions.removeUninstalledModulesFromDesired} onChange={(checked) => setRequestOptions((current) => ({ ...current, removeUninstalledModulesFromDesired: checked }))} disabled={!canWrite || planBusy} />
|
<ToggleSwitch label="i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b" checked={requestOptions.removeUninstalledModulesFromDesired} onChange={(checked) => setRequestOptions((current) => ({ ...current, removeUninstalledModulesFromDesired: checked }))} disabled={!canWrite || planBusy} />
|
||||||
<label><span>Health URLs</span><textarea value={requestOptions.healthUrls} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, healthUrls: event.target.value }))} placeholder="http://127.0.0.1:8000/health" /></label>
|
<label><span>i18n:govoplan-admin.health_urls.3b86647c</span><textarea value={requestOptions.healthUrls} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, healthUrls: event.target.value }))} placeholder="http://127.0.0.1:8000/health" /></label>
|
||||||
<label><span>Restart commands</span><textarea value={requestOptions.restartCommands} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, restartCommands: event.target.value }))} placeholder="systemctl restart govoplan.service" /></label>
|
<label><span>i18n:govoplan-admin.restart_commands.035997f9</span><textarea value={requestOptions.restartCommands} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, restartCommands: event.target.value }))} placeholder="systemctl restart govoplan.service" /></label>
|
||||||
<label><span>DB backup command</span><textarea value={requestOptions.databaseBackupCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseBackupCommand: event.target.value }))} placeholder={"pg_dump --format=custom \"$GOVOPLAN_DATABASE_URL\" > \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
|
<label><span>i18n:govoplan-admin.db_backup_command.5bded3ac</span><textarea value={requestOptions.databaseBackupCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseBackupCommand: event.target.value }))} placeholder={"pg_dump --format=custom \"$GOVOPLAN_DATABASE_URL\" > \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
|
||||||
<label><span>DB restore command</span><textarea value={requestOptions.databaseRestoreCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCommand: event.target.value }))} placeholder={"pg_restore --clean --if-exists --dbname \"$GOVOPLAN_DATABASE_URL\" \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
|
<label><span>i18n:govoplan-admin.db_restore_command.de11eb01</span><textarea value={requestOptions.databaseRestoreCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCommand: event.target.value }))} placeholder={"pg_restore --clean --if-exists --dbname \"$GOVOPLAN_DATABASE_URL\" \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
|
||||||
<label><span>DB restore-check command</span><textarea value={requestOptions.databaseRestoreCheckCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCheckCommand: event.target.value }))} placeholder={"pg_restore --list \"$GOVOPLAN_DATABASE_BACKUP_PATH\" >/dev/null"} /></label>
|
<label><span>i18n:govoplan-admin.db_restore_check_command.750ae936</span><textarea value={requestOptions.databaseRestoreCheckCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCheckCommand: event.target.value }))} placeholder={"pg_restore --list \"$GOVOPLAN_DATABASE_BACKUP_PATH\" >/dev/null"} /></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
@@ -428,88 +519,113 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
|
|||||||
{installerRequests && <div className="module-installer-runs">
|
{installerRequests && <div className="module-installer-runs">
|
||||||
<div className="module-installer-runs-heading">
|
<div className="module-installer-runs-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>Installer requests</h2>
|
<h2>i18n:govoplan-admin.installer_requests.b88db439</h2>
|
||||||
<p>Queued daemon handoffs created from the admin UI or CLI.</p>
|
<p>i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914</p>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={installerRequests.daemon.running ? statusTone(installerRequests.daemon.status) : "inactive"}
|
status={installerRequests.daemon.running ? statusTone(installerRequests.daemon.status) : "inactive"}
|
||||||
label={installerRequests.daemon.running ? `Daemon: ${installerRequests.daemon.status}` : "Daemon offline"}
|
label={installerRequests.daemon.running ? i18nMessage("i18n:govoplan-admin.daemon_value.e54f5a31", { value0: installerRequests.daemon.status }) : "i18n:govoplan-admin.daemon_offline.314f19a4"} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{installerRequests.requests.length === 0 && <div className="module-install-plan-empty">No installer requests recorded yet.</div>}
|
{installerRequests.requests.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa</div>}
|
||||||
{installerRequests.requests.length > 0 && <div className="module-installer-run-list">
|
{installerRequests.requests.length > 0 && <div className="module-installer-run-list">
|
||||||
{installerRequests.requests.map((request) => (
|
{installerRequests.requests.map((request) =>
|
||||||
<div key={request.request_id} className="module-installer-run-row">
|
<div key={request.request_id} className="module-installer-run-row">
|
||||||
<div className="module-installer-run-main">
|
<div className="module-installer-run-main">
|
||||||
<div className="module-installer-run-title">
|
<div className="module-installer-run-title">
|
||||||
<strong>{request.request_id}</strong>
|
<strong>{request.request_id}</strong>
|
||||||
<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>Created: {formatDateTime(request.created_at)}</span>
|
<span>i18n:govoplan-admin.created.0c78dab1 {formatDateTime(request.created_at)}</span>
|
||||||
<span>Started: {formatDateTime(request.started_at)}</span>
|
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(request.started_at)}</span>
|
||||||
<span>Finished: {formatDateTime(request.finished_at)}</span>
|
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(request.finished_at)}</span>
|
||||||
{request.cancelled_at && <span>Cancelled: {formatDateTime(request.cancelled_at)}</span>}
|
{request.cancelled_at && <span>i18n:govoplan-admin.cancelled.8c9dcfa8 {formatDateTime(request.cancelled_at)}</span>}
|
||||||
{request.retry_of && <span>Retry of: {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>}
|
||||||
</div>
|
</div>
|
||||||
{request.error && <p className="module-installer-run-error">{request.error}</p>}
|
{request.error && <p className="module-installer-run-error">{request.error}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="module-installer-run-actions">
|
<div className="module-installer-run-actions">
|
||||||
{request.status === "queued" && <Button onClick={() => void cancelInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>Cancel</Button>}
|
{request.status === "queued" && <Button onClick={() => void cancelInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>i18n:govoplan-admin.cancel.77dfd213</Button>}
|
||||||
{(request.status === "failed" || request.status === "cancelled") && <Button onClick={() => void retryInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>Retry</Button>}
|
{(request.status === "failed" || request.status === "cancelled") && <Button onClick={() => void retryInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>i18n:govoplan-admin.retry.9f5cd8a2</Button>}
|
||||||
{request.record_path && <code>{request.record_path}</code>}
|
{request.record_path && <code>{request.record_path}</code>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{installerRuns && <div className="module-installer-runs">
|
{installerRuns && <div className="module-installer-runs">
|
||||||
<div className="module-installer-runs-heading">
|
<div className="module-installer-runs-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>Installer runs</h2>
|
<h2>i18n:govoplan-admin.installer_runs.e9e344e1</h2>
|
||||||
<p>Recent supervised installer records and the current package-install lock.</p>
|
<p>i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245</p>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge status={installerRuns.lock.locked ? "warning" : "success"} label={installerRuns.lock.locked ? "Locked" : "Unlocked"} />
|
<StatusBadge status={installerRuns.lock.locked ? "warning" : "success"} label={installerRuns.lock.locked ? "i18n:govoplan-admin.locked.a798882f" : "i18n:govoplan-admin.unlocked.b3da7025"} />
|
||||||
</div>
|
</div>
|
||||||
{installerRuns.runs.length === 0 && <div className="module-install-plan-empty">No installer runs recorded yet.</div>}
|
{installerRuns.runs.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964</div>}
|
||||||
{installerRuns.runs.length > 0 && <div className="module-installer-run-list">
|
{installerRuns.runs.length > 0 && <div className="module-installer-run-list">
|
||||||
{installerRuns.runs.map((run) => (
|
{installerRuns.runs.map((run) =>
|
||||||
<div key={run.run_id} className="module-installer-run-row">
|
<div key={run.run_id} className="module-installer-run-row">
|
||||||
<div className="module-installer-run-main">
|
<div className="module-installer-run-main">
|
||||||
<div className="module-installer-run-title">
|
<div className="module-installer-run-title">
|
||||||
<strong>{run.run_id}</strong>
|
<strong>{run.run_id}</strong>
|
||||||
<StatusBadge status={statusTone(run.status)} label={run.status} />
|
<StatusBadge status={statusTone(run.status)} label={run.status} />
|
||||||
{run.supervisor_status && <StatusBadge status={statusTone(run.supervisor_status)} label={`Supervisor: ${run.supervisor_status}`} />}
|
{run.supervisor_status && <StatusBadge status={statusTone(run.supervisor_status)} label={i18nMessage("i18n:govoplan-admin.supervisor_value.d9f0c8eb", { value0: run.supervisor_status })} />}
|
||||||
{run.rollback_status && <StatusBadge status={statusTone(run.rollback_status)} label={`Rollback: ${run.rollback_status}`} />}
|
{run.rollback_status && <StatusBadge status={statusTone(run.rollback_status)} label={i18nMessage("i18n:govoplan-admin.rollback_value.47e9c00b", { value0: run.rollback_status })} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="module-management-details">
|
<div className="module-management-details">
|
||||||
<span>Modules: {run.planned_modules.length ? run.planned_modules.join(", ") : "none"}</span>
|
<span>i18n:govoplan-admin.modules.2dc3b299 {run.planned_modules.length ? run.planned_modules.join(", ") : "none"}</span>
|
||||||
<span>Commands: {run.commands_count}</span>
|
{run.request_id && <span>i18n:govoplan-admin.request.43c2e7a7 {run.request_id}</span>}
|
||||||
<span>Started: {formatDateTime(run.started_at)}</span>
|
{traceId(run.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(run.trace)}</code></span>}
|
||||||
<span>Finished: {formatDateTime(run.finished_at)}</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.finished.4b52fe3f {formatDateTime(run.finished_at)}</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>
|
||||||
<code>{run.record_path}</code>
|
<code>{run.record_path}</code>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>}
|
</div>}
|
||||||
</>}
|
</>}
|
||||||
</AdminPageLayout>
|
</AdminPageLayout>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModuleMetric({ label, value, detail, tone = "info" }: { label: string; value: string | number; detail: string; tone?: "info" | "good" | "warning" }) {
|
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>;
|
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 ModuleStatus({ module, desiredEnabled }: { module: ModuleCatalogItem; desiredEnabled: boolean }) {
|
function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) {
|
||||||
if (module.current_enabled && !desiredEnabled) return <StatusBadge status="warning" label="Will disable" />;
|
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";
|
||||||
if (!module.current_enabled && desiredEnabled) return <StatusBadge status="warning" label="Will enable" />;
|
return (
|
||||||
if (module.current_enabled) return <StatusBadge status="success" label="Active" />;
|
<div className="module-license-status">
|
||||||
return <StatusBadge status="inactive" label="Inactive" />;
|
<div className="button-row compact-actions">
|
||||||
|
<StatusBadge status={licenseTone(license)} label={licenseLabel(license)} />
|
||||||
|
<StatusBadge status={license.enforced ? "warning" : "inactive"} label={license.enforced ? "i18n:govoplan-admin.enforced.93ff909c" : "i18n:govoplan-admin.observe_only.c6a1f8bc"} />
|
||||||
|
{license.key_id && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.key_value.847b914f", { value0: license.key_id })} />}
|
||||||
|
</div>
|
||||||
|
<p className="module-package-catalog-description">{label}</p>
|
||||||
|
<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.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.missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {license.missing_features.join(", ")}</span>}
|
||||||
|
</div>
|
||||||
|
{license.reason && <p className={`module-package-catalog-description${license.allowed ? "" : " alert warning"}`}>{license.reason}</p>}
|
||||||
|
{license.guidance.map((item) => <p key={item} className="module-package-catalog-description">{item}</p>)}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModuleStatus({ module, desiredEnabled }: {module: ModuleCatalogItem;desiredEnabled: boolean;}) {
|
||||||
|
if (module.current_enabled && !desiredEnabled) return <StatusBadge status="warning" label="i18n:govoplan-admin.will_disable.14bb193a" />;
|
||||||
|
if (!module.current_enabled && desiredEnabled) return <StatusBadge status="warning" label="i18n:govoplan-admin.will_enable.5d52d70b" />;
|
||||||
|
if (module.current_enabled) return <StatusBadge status="success" label="i18n:govoplan-admin.active.a733b809" />;
|
||||||
|
return <StatusBadge status="inactive" label="i18n:govoplan-admin.inactive.09af574c" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameSet(left: ReadonlySet<string>, right: ReadonlySet<string>) {
|
function sameSet(left: ReadonlySet<string>, right: ReadonlySet<string>) {
|
||||||
@@ -535,19 +651,19 @@ function emptyPlanItem(): ModuleInstallPlanItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizePlanItems(items: ModuleInstallPlanItem[]): ModuleInstallPlanItem[] {
|
function normalizePlanItems(items: ModuleInstallPlanItem[]): ModuleInstallPlanItem[] {
|
||||||
return items
|
return items.
|
||||||
.map((item) => ({
|
map((item) => ({
|
||||||
module_id: item.module_id.trim(),
|
module_id: item.module_id.trim(),
|
||||||
action: item.action,
|
action: item.action,
|
||||||
python_package: cleanOptional(item.python_package),
|
python_package: cleanOptional(item.python_package),
|
||||||
python_ref: item.action === "install" ? cleanOptional(item.python_ref) : null,
|
python_ref: item.action === "install" ? cleanOptional(item.python_ref) : null,
|
||||||
webui_package: cleanOptional(item.webui_package),
|
webui_package: cleanOptional(item.webui_package),
|
||||||
webui_ref: item.action === "install" ? cleanOptional(item.webui_ref) : null,
|
webui_ref: item.action === "install" ? cleanOptional(item.webui_ref) : null,
|
||||||
destroy_data: item.action === "uninstall" && Boolean(item.destroy_data),
|
destroy_data: item.action === "uninstall" && Boolean(item.destroy_data),
|
||||||
status: item.status,
|
status: item.status,
|
||||||
notes: cleanOptional(item.notes)
|
notes: cleanOptional(item.notes)
|
||||||
}))
|
})).
|
||||||
.filter((item) => item.module_id || item.python_package || item.python_ref || item.webui_package || item.webui_ref || item.notes);
|
filter((item) => item.module_id || item.python_package || item.python_ref || item.webui_package || item.webui_ref || item.notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanOptional(value?: string | null): string | null {
|
function cleanOptional(value?: string | null): string | null {
|
||||||
@@ -557,12 +673,12 @@ function cleanOptional(value?: string | null): string | null {
|
|||||||
|
|
||||||
function planValidationError(items: ModuleInstallPlanItem[]): string {
|
function planValidationError(items: ModuleInstallPlanItem[]): string {
|
||||||
for (const item of normalizePlanItems(items)) {
|
for (const item of normalizePlanItems(items)) {
|
||||||
if (!item.module_id) return "Every plan item needs a module id.";
|
if (!item.module_id) return "i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206";
|
||||||
if (item.action === "install" && !item.python_ref) return `${item.module_id} needs a Python package reference.`;
|
if (item.action === "install" && !item.python_ref) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e", { value0: item.module_id });
|
||||||
if (item.action === "install" && item.python_ref && !item.python_package) return `${item.module_id} needs a Python package name for rollback.`;
|
if (item.action === "install" && item.python_ref && !item.python_package) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75", { value0: item.module_id });
|
||||||
if (item.action === "uninstall" && !item.python_package) return `${item.module_id} needs a Python package name for uninstall.`;
|
if (item.action === "uninstall" && !item.python_package) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa", { value0: item.module_id });
|
||||||
if (item.action === "install" && Boolean(item.webui_package) !== Boolean(item.webui_ref)) return `${item.module_id} needs both WebUI package and WebUI reference, or neither.`;
|
if (item.action === "install" && Boolean(item.webui_package) !== Boolean(item.webui_ref)) return i18nMessage("i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073", { value0: item.module_id });
|
||||||
if (item.action === "uninstall" && item.webui_ref && !item.webui_package) return `${item.module_id} has a WebUI reference but no WebUI package.`;
|
if (item.action === "uninstall" && item.webui_ref && !item.webui_package) return i18nMessage("i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149", { value0: item.module_id });
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -575,20 +691,49 @@ function statusTone(status: string): string {
|
|||||||
return "inactive";
|
return "inactive";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function licenseTone(license: ModuleLicenseDiagnostics): string {
|
||||||
|
if (!license.valid || !license.allowed || license.missing_features.length > 0) return "error";
|
||||||
|
if (!license.configured || !license.trusted || license.expires_in_days !== null && license.expires_in_days !== undefined && license.expires_in_days <= 30) return "warning";
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
function licenseLabel(license: ModuleLicenseDiagnostics): string {
|
||||||
|
if (!license.configured) return "i18n:govoplan-admin.no_license.eb4bfe19";
|
||||||
|
if (!license.valid) return "i18n:govoplan-admin.invalid_license.f16bf749";
|
||||||
|
if (license.missing_features.length > 0) return "i18n:govoplan-admin.missing_entitlement.0cafa611";
|
||||||
|
if (license.trusted) return "i18n:govoplan-admin.valid_and_trusted.73cf89bc";
|
||||||
|
if (license.signed) return "i18n:govoplan-admin.signed_untrusted.864ed50c";
|
||||||
|
return "i18n:govoplan-admin.unsigned.e91344ea";
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(value?: string | null): string {
|
function formatDateTime(value?: string | null): string {
|
||||||
if (!value) return "not recorded";
|
if (!value) return "i18n:govoplan-admin.not_recorded.9925ee3c";
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function traceId(value?: Record<string, unknown> | null): string {
|
||||||
|
const correlationId = value?.correlation_id;
|
||||||
|
return typeof correlationId === "string" && correlationId.trim() ? correlationId : "";
|
||||||
|
}
|
||||||
|
|
||||||
function installerCommand(frontendRebuildRequired: boolean): string {
|
function installerCommand(frontendRebuildRequired: boolean): string {
|
||||||
const buildFlag = frontendRebuildRequired ? " --build-webui" : "";
|
const buildFlag = frontendRebuildRequired ? " --build-webui" : "";
|
||||||
return [
|
return [
|
||||||
"govoplan-module-installer --format shell",
|
"govoplan-module-installer --format shell",
|
||||||
`govoplan-module-installer --daemon --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`,
|
`govoplan-module-installer --daemon --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`,
|
||||||
`govoplan-module-installer --enqueue-supervised --migrate${buildFlag} --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`
|
`govoplan-module-installer --enqueue-supervised --migrate${buildFlag} --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`].
|
||||||
].join("\n");
|
join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModuleStateRequestState = {
|
||||||
|
request: ConfigurationChangeRequest;
|
||||||
|
payloadKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function moduleStatePayloadKey(enabledModules: string[]): string {
|
||||||
|
return JSON.stringify(enabledModules);
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstallerRequestFormOptions = {
|
type InstallerRequestFormOptions = {
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { Button } from "@govoplan/core-webui";
|
|||||||
import { Card } from "@govoplan/core-webui";
|
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 { fetchSystemSettings, updateSystemSettings, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsItem } from "../../api/admin";
|
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin";
|
||||||
import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui";
|
import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
const DELTA_KEY = "admin:system-settings";
|
||||||
|
|
||||||
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
|
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
|
||||||
store_raw_campaign_json: true,
|
store_raw_campaign_json: true,
|
||||||
@@ -35,86 +37,220 @@ const fallback: SystemSettingsItem = {
|
|||||||
allow_tenant_api_keys: true,
|
allow_tenant_api_keys: true,
|
||||||
privacy_retention_policy: defaultPrivacyPolicy,
|
privacy_retention_policy: defaultPrivacyPolicy,
|
||||||
maintenance_mode: { enabled: false, message: "" },
|
maintenance_mode: { enabled: false, message: "" },
|
||||||
|
available_languages: [
|
||||||
|
{ code: "en", label: "English", native_label: "English" },
|
||||||
|
{ code: "de", label: "German", native_label: "Deutsch" }
|
||||||
|
],
|
||||||
|
enabled_language_codes: ["en", "de"],
|
||||||
settings: {}
|
settings: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: { settings: ApiSettings; canWrite: boolean; canAccessMaintenance: boolean }) {
|
export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) {
|
||||||
const [draft, setDraft] = useState<SystemSettingsItem>(fallback);
|
const [draft, setDraft] = useState<SystemSettingsItem>(fallback);
|
||||||
|
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(fallback);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
|
const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" });
|
||||||
|
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
||||||
|
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
|
||||||
|
const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
onSave: save,
|
||||||
|
onDiscard: () => {
|
||||||
|
setDraft(savedDraft);
|
||||||
|
setPackageDraft({ code: "", label: "", nativeLabel: "" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const loaded = await fetchSystemSettings(settings);
|
const wasDirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
|
||||||
setDraft(loaded);
|
const response = await fetchSystemSettingsDelta(settings, { since: getDeltaWatermark(DELTA_KEY) });
|
||||||
|
setDeltaWatermark(DELTA_KEY, response.watermark);
|
||||||
|
if (response.full && response.item) {
|
||||||
|
setSavedDraft(response.item);
|
||||||
|
if (!wasDirty) setDraft(response.item);
|
||||||
|
} else if (response.changed_sections.length) {
|
||||||
|
setSavedDraft((current) => applySystemSettingsSections(current, response.sections));
|
||||||
|
if (!wasDirty) setDraft((current) => applySystemSettingsSections(current, response.sections));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (err) { setError(adminErrorMessage(err)); }
|
catch (err) {setError(adminErrorMessage(err));} finally
|
||||||
finally { setLoading(false); }
|
{setLoading(false);}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
|
useEffect(() => {
|
||||||
|
resetDeltaWatermark(DELTA_KEY);
|
||||||
|
void load();
|
||||||
|
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
|
||||||
|
|
||||||
async function save() {
|
async function save(): Promise<boolean> {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
setDraft(await updateSystemSettings(settings, {
|
const saved = await updateSystemSettings(settings, {
|
||||||
default_locale: draft.default_locale,
|
default_locale: draft.default_locale,
|
||||||
allow_tenant_custom_groups: draft.allow_tenant_custom_groups,
|
allow_tenant_custom_groups: draft.allow_tenant_custom_groups,
|
||||||
allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
|
allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
|
||||||
allow_tenant_api_keys: draft.allow_tenant_api_keys,
|
allow_tenant_api_keys: draft.allow_tenant_api_keys,
|
||||||
maintenance_mode: draft.maintenance_mode
|
maintenance_mode: draft.maintenance_mode,
|
||||||
}));
|
available_languages: draft.available_languages,
|
||||||
setSuccess("System settings saved.");
|
enabled_language_codes: draft.enabled_language_codes
|
||||||
} catch (err) { setError(adminErrorMessage(err)); }
|
});
|
||||||
finally { setBusy(false); }
|
setDraft(saved);
|
||||||
|
setSavedDraft(saved);
|
||||||
|
resetDeltaWatermark(DELTA_KEY);
|
||||||
|
setSuccess("i18n:govoplan-admin.system_settings_saved.dac4f17b");
|
||||||
|
return true;
|
||||||
|
} catch (err) {setError(adminErrorMessage(err));return false;} finally
|
||||||
|
{setBusy(false);}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLanguage(code: string, checked: boolean) {
|
||||||
|
const enabled = new Set(draft.enabled_language_codes);
|
||||||
|
if (checked) enabled.add(code);
|
||||||
|
else enabled.delete(code);
|
||||||
|
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);
|
||||||
|
setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale });
|
||||||
|
}
|
||||||
|
|
||||||
|
function installLanguagePackage() {
|
||||||
|
const code = normalizeLanguageCode(packageDraft.code);
|
||||||
|
if (!code || !packageDraft.label.trim()) return;
|
||||||
|
const nextPackage: LanguagePackage = {
|
||||||
|
code,
|
||||||
|
label: packageDraft.label.trim(),
|
||||||
|
native_label: packageDraft.nativeLabel.trim() || packageDraft.label.trim()
|
||||||
|
};
|
||||||
|
const existing = new Set(draft.available_languages.map((item) => item.code));
|
||||||
|
const available = existing.has(code)
|
||||||
|
? draft.available_languages.map((item) => item.code === code ? nextPackage : item)
|
||||||
|
: [...draft.available_languages, nextPackage];
|
||||||
|
const enabled = draft.enabled_language_codes.includes(code) ? draft.enabled_language_codes : [...draft.enabled_language_codes, code];
|
||||||
|
setDraft({ ...draft, available_languages: available, enabled_language_codes: enabled, default_locale: draft.default_locale || code });
|
||||||
|
setPackageDraft({ code: "", label: "", nativeLabel: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminPageLayout
|
<AdminPageLayout
|
||||||
title="System general settings"
|
title="i18n:govoplan-admin.system_general_settings.7cd662ed"
|
||||||
description="Instance-wide defaults and tenant governance capabilities. Retention policy management has its own system section."
|
description="i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
success={success}
|
success={success}
|
||||||
actions={<><Button onClick={() => void load()} disabled={loading}>Reload</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy}>{busy ? "Saving…" : "Save settings"}</Button></>}
|
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}>
|
||||||
>
|
|
||||||
<div className="admin-settings-form">
|
<div className="admin-settings-form">
|
||||||
<Card title="Defaults for newly created tenants">
|
<Card title="i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd">
|
||||||
<FormField label="Default locale"><input value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-admin.default_locale.b99d021f">
|
||||||
|
<select value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} disabled={!canWrite || busy || defaultLocaleOptions.length === 0}>
|
||||||
|
{defaultLocaleOptions.map((code) => {
|
||||||
|
const language = draft.available_languages.find((item) => item.code === code);
|
||||||
|
return <option key={code} value={code}>{languageOptionLabel(language ?? { code, label: code.toUpperCase() })}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Tenant administration capabilities">
|
<Card title="i18n:govoplan-admin.language_packages">
|
||||||
<div className="settings-list">
|
<div className="settings-list">
|
||||||
<ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} label="Allow tenant-defined groups by default" />
|
{draft.available_languages.map((item) => (
|
||||||
<ToggleSwitch checked={draft.allow_tenant_custom_roles} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_roles: checked })} label="Allow tenant-defined roles by default" />
|
<label className="admin-inline-check" key={item.code}>
|
||||||
<ToggleSwitch checked={draft.allow_tenant_api_keys} onChange={(checked) => setDraft({ ...draft, allow_tenant_api_keys: checked })} label="Allow tenant API keys by default" />
|
<input
|
||||||
|
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>
|
||||||
<p className="muted small-note">These settings are enforced by the backend. Central groups and tenant roles remain available even when local creation is disabled.</p>
|
<div className="form-grid">
|
||||||
|
<FormField label="i18n:govoplan-admin.language_code">
|
||||||
|
<input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="i18n:govoplan-admin.language_name">
|
||||||
|
<input value={packageDraft.label} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, label: event.target.value })} />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="i18n:govoplan-admin.native_language_name">
|
||||||
|
<input value={packageDraft.nativeLabel} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.native_language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} />
|
||||||
|
</FormField>
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<Button onClick={installLanguagePackage} disabled={!canWrite || busy || !normalizeLanguageCode(packageDraft.code) || !packageDraft.label.trim()}>i18n:govoplan-admin.install_language_package</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="muted small-note">i18n:govoplan-admin.language_packages_help</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Maintenance mode">
|
<Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972">
|
||||||
|
<div className="settings-list">
|
||||||
|
<ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} label="i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a" />
|
||||||
|
<ToggleSwitch checked={draft.allow_tenant_custom_roles} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_roles: checked })} label="i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9" />
|
||||||
|
<ToggleSwitch checked={draft.allow_tenant_api_keys} onChange={(checked) => setDraft({ ...draft, allow_tenant_api_keys: checked })} label="i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17" />
|
||||||
|
</div>
|
||||||
|
<p className="muted small-note">i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f</p>
|
||||||
|
</Card>
|
||||||
|
<Card title="i18n:govoplan-admin.maintenance_mode.98cca5c6">
|
||||||
<div className="settings-list">
|
<div className="settings-list">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={draft.maintenance_mode.enabled}
|
checked={draft.maintenance_mode.enabled}
|
||||||
disabled={!canWrite || !canAccessMaintenance}
|
disabled={!canWrite || !canAccessMaintenance}
|
||||||
onChange={(checked) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, enabled: checked } })}
|
onChange={(checked) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, enabled: checked } })}
|
||||||
label="Restrict authenticated API access to maintenance operators"
|
label="i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<FormField label="Maintenance message">
|
<FormField label="i18n:govoplan-admin.maintenance_message.ca62571f">
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={draft.maintenance_mode.message ?? ""}
|
value={draft.maintenance_mode.message ?? ""}
|
||||||
disabled={!canWrite}
|
disabled={!canWrite}
|
||||||
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })}
|
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })} />
|
||||||
/>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
<p className="muted small-note">Changing the maintenance-mode flag requires system:maintenance:access. Login remains available so an operator can sign in during maintenance.</p>
|
<p className="muted small-note">i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</AdminPageLayout>
|
</AdminPageLayout>);
|
||||||
);
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLanguageCode(value: string): string {
|
||||||
|
return value.trim().toLowerCase().replace(/_/g, "-").split(/[^a-z0-9]+/).filter(Boolean).join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function localeOptions(current: string, enabled: string[]): string[] {
|
||||||
|
return [...new Set([current, ...enabled].filter((item) => item && item.trim()))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageOptionLabel(language: LanguagePackage): string {
|
||||||
|
return `${language.code.toUpperCase()} - ${language.native_label || language.label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemSettingsDraftKey(item: SystemSettingsItem): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
default_locale: item.default_locale,
|
||||||
|
allow_tenant_custom_groups: item.allow_tenant_custom_groups,
|
||||||
|
allow_tenant_custom_roles: item.allow_tenant_custom_roles,
|
||||||
|
allow_tenant_api_keys: item.allow_tenant_api_keys,
|
||||||
|
maintenance_mode: item.maintenance_mode,
|
||||||
|
available_languages: item.available_languages,
|
||||||
|
enabled_language_codes: item.enabled_language_codes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySystemSettingsSections(item: SystemSettingsItem, sections: SystemSettingsDeltaSections): SystemSettingsItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...(sections.defaults ?? {}),
|
||||||
|
...(sections.tenant_capabilities ?? {}),
|
||||||
|
...(sections.languages ?? {}),
|
||||||
|
...(sections.privacy_retention_policy ? { privacy_retention_policy: sections.privacy_retention_policy } : {}),
|
||||||
|
...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}),
|
||||||
|
...(sections.settings ? { settings: sections.settings } : {})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
710
webui/src/i18n/generatedTranslations.ts
Normal file
710
webui/src/i18n/generatedTranslations.ts
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
|
"en": {
|
||||||
|
"i18n:govoplan-admin.access_configuration_exported.098f200d": "Access configuration exported.",
|
||||||
|
"i18n:govoplan-admin.action.97c89a4d": "Action",
|
||||||
|
"i18n:govoplan-admin.actions.c3cd636a": "Actions",
|
||||||
|
"i18n:govoplan-admin.activate_installs.731d22a1": "Activate installs",
|
||||||
|
"i18n:govoplan-admin.active_and_total_memberships.c0c20f10": "Active and total memberships.",
|
||||||
|
"i18n:govoplan-admin.active_tenant_automation_credentials.240c659e": "Active tenant automation credentials.",
|
||||||
|
"i18n:govoplan-admin.active_tenant.dfadf0aa": "Active tenant:",
|
||||||
|
"i18n:govoplan-admin.active.a733b809": "Active",
|
||||||
|
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
|
||||||
|
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
|
||||||
|
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add tenant role",
|
||||||
|
"i18n:govoplan-admin.admin.4e7afebc": "Admin",
|
||||||
|
"i18n:govoplan-admin.administration.b8be3d12": "Administration",
|
||||||
|
"i18n:govoplan-admin.platform_administration": "Platform administration",
|
||||||
|
"i18n:govoplan-admin.alert_warning.804f617a": " alert warning",
|
||||||
|
"i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17": "Allow tenant API keys by default",
|
||||||
|
"i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a": "Allow tenant-defined groups by default",
|
||||||
|
"i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9": "Allow tenant-defined roles by default",
|
||||||
|
"i18n:govoplan-admin.api_keys.94fcf3c2": "API keys",
|
||||||
|
"i18n:govoplan-admin.applied.a3e4a569": "Applied",
|
||||||
|
"i18n:govoplan-admin.apply_approved_request.ab218d9a": "Apply approved request",
|
||||||
|
"i18n:govoplan-admin.apply_finished_with_blockers.78487a12": "Apply finished with blockers.",
|
||||||
|
"i18n:govoplan-admin.apply_result.0fde1c3c": "Apply result",
|
||||||
|
"i18n:govoplan-admin.apply.cfea419c": "Apply",
|
||||||
|
"i18n:govoplan-admin.approvals.deb9d03c": "Approvals",
|
||||||
|
"i18n:govoplan-admin.approve.7b2c7f14": "Approve",
|
||||||
|
"i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16": "Approved package references that can be added to the operator install plan.",
|
||||||
|
"i18n:govoplan-admin.approved_value.52da808b": "Approved {value0}.",
|
||||||
|
"i18n:govoplan-admin.approvers.0e2de1fb": "Approvers",
|
||||||
|
"i18n:govoplan-admin.audit.fa1703dd": "Audit",
|
||||||
|
"i18n:govoplan-admin.available.7c62a142": "Available",
|
||||||
|
"i18n:govoplan-admin.blocked.99613c74": "Blocked",
|
||||||
|
"i18n:govoplan-admin.build_webui.ed5ef1fd": " --build-webui",
|
||||||
|
"i18n:govoplan-admin.build_webui.fe8ccad7": "Build WebUI",
|
||||||
|
"i18n:govoplan-admin.bypass_allowed.4e347c27": "Bypass allowed",
|
||||||
|
"i18n:govoplan-admin.bypass_denied.ab987400": "Bypass denied",
|
||||||
|
"i18n:govoplan-admin.cached_from.d73c5b2a": "· cached from",
|
||||||
|
"i18n:govoplan-admin.cancel.77dfd213": "Cancel",
|
||||||
|
"i18n:govoplan-admin.cancelled.8c9dcfa8": "Cancelled:",
|
||||||
|
"i18n:govoplan-admin.case_clerk.b78a314a": "Case clerk",
|
||||||
|
"i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f": "Catalog install entry planned.",
|
||||||
|
"i18n:govoplan-admin.catalog.4a88d27b": "Catalog",
|
||||||
|
"i18n:govoplan-admin.central_groups.5c9b5b66": "Central groups",
|
||||||
|
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9": "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local.",
|
||||||
|
"i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1": "Centrally governed tenant roles and their availability across tenants.",
|
||||||
|
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3": "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.",
|
||||||
|
"i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b": "Centrally governed tenant roles.",
|
||||||
|
"i18n:govoplan-admin.change_request_created_value.4af6d3d2": "Change request created: {value0}",
|
||||||
|
"i18n:govoplan-admin.change_request_id.96ee3239": "Change request id",
|
||||||
|
"i18n:govoplan-admin.change_request_ready.b17e8c57": "Change request ready:",
|
||||||
|
"i18n:govoplan-admin.changes.8aa57de6": "Changes",
|
||||||
|
"i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed": "Changing enabled modules requires a dry-run change request and active maintenance mode.",
|
||||||
|
"i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f": "Changing the maintenance-mode flag requires system:maintenance:access. Login remains available so an operator can sign in during maintenance.",
|
||||||
|
"i18n:govoplan-admin.channel_value.d3894efe": "Channel: {value0}",
|
||||||
|
"i18n:govoplan-admin.clear.719ea396": "Clear",
|
||||||
|
"i18n:govoplan-admin.close.bbfa773e": "Close",
|
||||||
|
"i18n:govoplan-admin.code.adac6937": "Code",
|
||||||
|
"i18n:govoplan-admin.commands.f6a34aed": "Commands:",
|
||||||
|
"i18n:govoplan-admin.configuration_changes.82933bbb": "Configuration changes",
|
||||||
|
"i18n:govoplan-admin.configuration_package_apply.c270e5f3": "Configuration package apply",
|
||||||
|
"i18n:govoplan-admin.configuration_packages.eb2f05f1": "Configuration packages",
|
||||||
|
"i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335": "Configuration requests, approvals and version history.",
|
||||||
|
"i18n:govoplan-admin.configured_license.3e73f8f5": "Configured license",
|
||||||
|
"i18n:govoplan-admin.create_dry_run_request.05b9cbdb": "Create dry-run request",
|
||||||
|
"i18n:govoplan-admin.create_group_template.72407248": "Create group template",
|
||||||
|
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply",
|
||||||
|
"i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39": "Create, suspend and govern tenant spaces.",
|
||||||
|
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create tenant role",
|
||||||
|
"i18n:govoplan-admin.created.0c78dab1": "Created:",
|
||||||
|
"i18n:govoplan-admin.created.accf40c8": "Created",
|
||||||
|
"i18n:govoplan-admin.daemon_execution.cc0fad8d": "Daemon execution",
|
||||||
|
"i18n:govoplan-admin.daemon_offline.314f19a4": "Daemon offline",
|
||||||
|
"i18n:govoplan-admin.daemon_value.e54f5a31": "Daemon: {value0}",
|
||||||
|
"i18n:govoplan-admin.db_backup_command.5bded3ac": "DB backup command",
|
||||||
|
"i18n:govoplan-admin.db_restore_check_command.750ae936": "DB restore-check command",
|
||||||
|
"i18n:govoplan-admin.db_restore_command.de11eb01": "DB restore command",
|
||||||
|
"i18n:govoplan-admin.db.e355c23a": "DB:",
|
||||||
|
"i18n:govoplan-admin.default_locale.b99d021f": "Default locale",
|
||||||
|
"i18n:govoplan-admin.install_language_package": "Install language package",
|
||||||
|
"i18n:govoplan-admin.language_code": "Language code",
|
||||||
|
"i18n:govoplan-admin.language_name": "Language name",
|
||||||
|
"i18n:govoplan-admin.language_name_placeholder": "French",
|
||||||
|
"i18n:govoplan-admin.language_packages": "Language packages",
|
||||||
|
"i18n:govoplan-admin.language_packages_help": "Installed packages define which language codes can be enabled. A package only becomes fully translated when matching module translation catalogs are present.",
|
||||||
|
"i18n:govoplan-admin.native_language_name": "Native language name",
|
||||||
|
"i18n:govoplan-admin.native_language_name_placeholder": "Francais",
|
||||||
|
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Defaults for newly created tenants",
|
||||||
|
"i18n:govoplan-admin.delete_group_template.8745d842": "Delete group template",
|
||||||
|
"i18n:govoplan-admin.delete_template.399bf72a": "Delete template",
|
||||||
|
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete tenant role",
|
||||||
|
"i18n:govoplan-admin.delete_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.dependents.072665c4": "Dependents:",
|
||||||
|
"i18n:govoplan-admin.description.55f8ebc8": "Description",
|
||||||
|
"i18n:govoplan-admin.destroy_data.34557c3c": "Destroy data",
|
||||||
|
"i18n:govoplan-admin.disabled.f4f4473d": "Disabled",
|
||||||
|
"i18n:govoplan-admin.discovered_packages.660902de": "Discovered packages",
|
||||||
|
"i18n:govoplan-admin.drift.4876f7b9": "Drift",
|
||||||
|
"i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b": "Drop uninstalls from startup",
|
||||||
|
"i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07": "Dry-run change request created: {value0}. Enable maintenance mode, then apply the request.",
|
||||||
|
"i18n:govoplan-admin.dry_run.3d14659c": "Dry-run",
|
||||||
|
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template",
|
||||||
|
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit tenant role",
|
||||||
|
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
|
||||||
|
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
|
||||||
|
"i18n:govoplan-admin.enabled.df174a3f": "Enabled",
|
||||||
|
"i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968": "Enabling maintenance mode requires system:maintenance:access.",
|
||||||
|
"i18n:govoplan-admin.enabling.2b8e03e6": "Enabling...",
|
||||||
|
"i18n:govoplan-admin.enforced.93ff909c": "Enforced",
|
||||||
|
"i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206": "Every plan item needs a module id.",
|
||||||
|
"i18n:govoplan-admin.expires.2d21b7de": "· Expires:",
|
||||||
|
"i18n:govoplan-admin.export_access.9a2eb91e": "Export access",
|
||||||
|
"i18n:govoplan-admin.export_finished_with_blockers.e2f70611": "Export finished with blockers.",
|
||||||
|
"i18n:govoplan-admin.export.f3e4fadb": "Export",
|
||||||
|
"i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c": "Exported system access configuration",
|
||||||
|
"i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c": "Exported tenant access configuration",
|
||||||
|
"i18n:govoplan-admin.features.5df81ffa": "Features:",
|
||||||
|
"i18n:govoplan-admin.file_connections.1e362326": "File connections",
|
||||||
|
"i18n:govoplan-admin.finished.4b52fe3f": "Finished:",
|
||||||
|
"i18n:govoplan-admin.fragment.3f19d616": "Fragment",
|
||||||
|
"i18n:govoplan-admin.front_office.d9dcfee1": "Front office",
|
||||||
|
"i18n:govoplan-admin.general.9239ee2c": "General",
|
||||||
|
"i18n:govoplan-admin.generated.a2edf57c": "Generated:",
|
||||||
|
"i18n:govoplan-admin.global": "Global",
|
||||||
|
"i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f": "Global accounts, tenant memberships and system-role assignments.",
|
||||||
|
"i18n:govoplan-admin.global_login_accounts_across_all_tenants.2aab129d": "Global login accounts across all tenants.",
|
||||||
|
"i18n:govoplan-admin.govoplan_module_installer_format_shell.226d1450": "govoplan-module-installer --format shell",
|
||||||
|
"i18n:govoplan-admin.group_definitions_made_available_or_required_in_.55402e16": "Group definitions made available or required in selected tenants.",
|
||||||
|
"i18n:govoplan-admin.group_file_connector_policy_limits": "File connector policy limits for group-owned spaces.",
|
||||||
|
"i18n:govoplan-admin.group_mail_server_policy_limits": "Mail server policy limits for group-owned work.",
|
||||||
|
"i18n:govoplan-admin.group_retention_policy_limits": "Retention limits for group-owned records.",
|
||||||
|
"i18n:govoplan-admin.group_template.973e0fa6": "Group template",
|
||||||
|
"i18n:govoplan-admin.group.171a0606": "Group",
|
||||||
|
"i18n:govoplan-admin.groups.ae9629f4": "Groups",
|
||||||
|
"i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349": "Handles incoming administrative work.",
|
||||||
|
"i18n:govoplan-admin.health_urls.3b86647c": "Health URLs",
|
||||||
|
"i18n:govoplan-admin.history.90ccd649": "History",
|
||||||
|
"i18n:govoplan-admin.id.474ae526": "Id",
|
||||||
|
"i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929": "Import, preflight, approve, apply and export module-owned configuration packages.",
|
||||||
|
"i18n:govoplan-admin.inactive.09af574c": "Inactive",
|
||||||
|
"i18n:govoplan-admin.inspect_value.9d5d1071": "Inspect {value0}",
|
||||||
|
"i18n:govoplan-admin.install.fd6c3ebf": "Install",
|
||||||
|
"i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd": "Installed modules, active runtime state, and saved startup state.",
|
||||||
|
"i18n:govoplan-admin.installed_modules_runtime_state_and_startup_stat.38dd7028": "Installed modules, runtime state and startup state.",
|
||||||
|
"i18n:govoplan-admin.installed.7bb4405c": "Installed",
|
||||||
|
"i18n:govoplan-admin.installer_preflight_blocked.cf83bf79": "Installer preflight blocked",
|
||||||
|
"i18n:govoplan-admin.installer_preflight_passed.6d5f8060": "Installer preflight passed",
|
||||||
|
"i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d": "Installer request cancelled: {value0}",
|
||||||
|
"i18n:govoplan-admin.installer_request_queued_value.00726abb": "Installer request queued: {value0}",
|
||||||
|
"i18n:govoplan-admin.installer_requests.b88db439": "Installer requests",
|
||||||
|
"i18n:govoplan-admin.installer_runs.e9e344e1": "Installer runs",
|
||||||
|
"i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa": "Instance defaults and tenant governance capabilities.",
|
||||||
|
"i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069": "Instance privacy retention policy and lower-level override permissions.",
|
||||||
|
"i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97": "Instance-wide defaults and tenant governance capabilities. Retention policy management has its own system section.",
|
||||||
|
"i18n:govoplan-admin.instance_wide_roles_assigned_directly_to_global_.91050488": "Instance-wide roles assigned directly to global accounts.",
|
||||||
|
"i18n:govoplan-admin.invalid_json.01ccb74f": "Invalid JSON.",
|
||||||
|
"i18n:govoplan-admin.invalid_license.f16bf749": "Invalid license",
|
||||||
|
"i18n:govoplan-admin.json_must_be_an_object.848569c9": "JSON must be an object.",
|
||||||
|
"i18n:govoplan-admin.key_value.847b914f": "Key: {value0}",
|
||||||
|
"i18n:govoplan-admin.key.c67dd20e": "Key",
|
||||||
|
"i18n:govoplan-admin.kind.e00ac23f": "Kind",
|
||||||
|
"i18n:govoplan-admin.label.74341e3c": "Label",
|
||||||
|
"i18n:govoplan-admin.last_request.4508ef35": "Last request:",
|
||||||
|
"i18n:govoplan-admin.license.de13bf1a": "License:",
|
||||||
|
"i18n:govoplan-admin.locked.a798882f": "Locked",
|
||||||
|
"i18n:govoplan-admin.mail_servers.d627326a": "Mail servers",
|
||||||
|
"i18n:govoplan-admin.maintenance_message.ca62571f": "Maintenance message",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c": "Maintenance mode enabled. Apply the module-state request when ready.",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629": "Maintenance mode is off. Package changes should be applied only after enabling maintenance mode.",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f": "Maintenance mode must be enabled before queueing a daemon request.",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode.98cca5c6": "Maintenance mode",
|
||||||
|
"i18n:govoplan-admin.maintenance.94de303b": "Maintenance",
|
||||||
|
"i18n:govoplan-admin.membership_status_groups_and_direct_roles_in_the.ab6c10b6": "Membership status, groups and direct roles in the active tenant.",
|
||||||
|
"i18n:govoplan-admin.message.68f4145f": "Message",
|
||||||
|
"i18n:govoplan-admin.minimal_office_access.af48f49a": "Minimal office access",
|
||||||
|
"i18n:govoplan-admin.missing_entitlement.0cafa611": "Missing entitlement",
|
||||||
|
"i18n:govoplan-admin.missing.feb2bbaa": "Missing:",
|
||||||
|
"i18n:govoplan-admin.module_install_plan_cleared.afa2698d": "Module install plan cleared.",
|
||||||
|
"i18n:govoplan-admin.module_install_plan_saved.5d43d944": "Module install plan saved.",
|
||||||
|
"i18n:govoplan-admin.module_maintenance_in_progress.f6395152": "Module maintenance in progress.",
|
||||||
|
"i18n:govoplan-admin.module_state_saved.e3e22105": "Module state saved.",
|
||||||
|
"i18n:govoplan-admin.module.b8ff0289": "Module",
|
||||||
|
"i18n:govoplan-admin.modules.04e9462c": "Modules",
|
||||||
|
"i18n:govoplan-admin.modules.2dc3b299": "Modules:",
|
||||||
|
"i18n:govoplan-admin.name.709a2322": "Name",
|
||||||
|
"i18n:govoplan-admin.needs_attention.a126722e": "Needs attention",
|
||||||
|
"i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7": "No applied configuration changes.",
|
||||||
|
"i18n:govoplan-admin.no_central_value_templates_found.081149fa": "No central {value0} templates found.",
|
||||||
|
"i18n:govoplan-admin.no_diagnostics.2b6e2630": "No diagnostics.",
|
||||||
|
"i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa": "No installer requests recorded yet.",
|
||||||
|
"i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964": "No installer runs recorded yet.",
|
||||||
|
"i18n:govoplan-admin.no_license_configured.693cea1a": "No license configured",
|
||||||
|
"i18n:govoplan-admin.no_license.eb4bfe19": "No license",
|
||||||
|
"i18n:govoplan-admin.no_open_requests.580c95f9": "No open requests.",
|
||||||
|
"i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3": "No package catalog configured. Set GOVOPLAN_MODULE_PACKAGE_CATALOG or GOVOPLAN_MODULE_PACKAGE_CATALOG_URL to enable curated install entries.",
|
||||||
|
"i18n:govoplan-admin.no_package_changes_planned.f12d8b33": "No package changes planned.",
|
||||||
|
"i18n:govoplan-admin.no_package_restart_pending.efe6b8ce": "No package restart pending",
|
||||||
|
"i18n:govoplan-admin.no_plan_items.7108c582": "No plan items.",
|
||||||
|
"i18n:govoplan-admin.none.6eef6648": "None",
|
||||||
|
"i18n:govoplan-admin.not_available.d1a17af1": "Not available",
|
||||||
|
"i18n:govoplan-admin.not_configured.811931bb": "Not configured",
|
||||||
|
"i18n:govoplan-admin.not_recorded.9925ee3c": "not recorded",
|
||||||
|
"i18n:govoplan-admin.notes.70440046": "Notes",
|
||||||
|
"i18n:govoplan-admin.object.2883f191": "Object",
|
||||||
|
"i18n:govoplan-admin.observe_only.c6a1f8bc": "Observe-only",
|
||||||
|
"i18n:govoplan-admin.off.e3de5ab0": "Off",
|
||||||
|
"i18n:govoplan-admin.on.e0049a66": "On",
|
||||||
|
"i18n:govoplan-admin.operator_install_plan.b203aabc": "Operator install plan",
|
||||||
|
"i18n:govoplan-admin.optional.166b8369": "Optional:",
|
||||||
|
"i18n:govoplan-admin.overview.0efc2e6b": "Overview",
|
||||||
|
"i18n:govoplan-admin.owner.89ff3122": "Owner",
|
||||||
|
"i18n:govoplan-admin.package_applied.63782ce7": "Package applied.",
|
||||||
|
"i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb": "Package catalog is configured but contains no entries.",
|
||||||
|
"i18n:govoplan-admin.package_catalog.b36e6d31": "Package catalog",
|
||||||
|
"i18n:govoplan-admin.package_json_must_be_an_object.db825bb3": "Package JSON must be an object.",
|
||||||
|
"i18n:govoplan-admin.package_json.a2b10f38": "Package JSON",
|
||||||
|
"i18n:govoplan-admin.package_uninstall_planned.56e8566d": "Package uninstall planned.",
|
||||||
|
"i18n:govoplan-admin.package.7431e3df": "Package",
|
||||||
|
"i18n:govoplan-admin.packages.0a999012": "Packages",
|
||||||
|
"i18n:govoplan-admin.pending.c515ec74": " pending",
|
||||||
|
"i18n:govoplan-admin.permissions.d06d5557": "Permissions",
|
||||||
|
"i18n:govoplan-admin.plan_install.e82bffe6": "Plan install",
|
||||||
|
"i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf": "Plan package installs and removals here, then apply the rendered commands from an operator shell during maintenance mode.",
|
||||||
|
"i18n:govoplan-admin.plan_uninstall.e62804ab": "Plan uninstall",
|
||||||
|
"i18n:govoplan-admin.plan.ae2f98a0": "Plan",
|
||||||
|
"i18n:govoplan-admin.planned.9cbe42aa": "Planned",
|
||||||
|
"i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8": "Preflight, approve, apply and export configuration packages.",
|
||||||
|
"i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b": "Preflight finished with blockers.",
|
||||||
|
"i18n:govoplan-admin.preflight_passed.c0c99055": "Preflight passed.",
|
||||||
|
"i18n:govoplan-admin.preflight.8016a487": "Preflight",
|
||||||
|
"i18n:govoplan-admin.python_package.34591c71": "Python package",
|
||||||
|
"i18n:govoplan-admin.python_ref.1af35d0d": "Python ref",
|
||||||
|
"i18n:govoplan-admin.queue_supervised_run.a53f248c": "Queue supervised run",
|
||||||
|
"i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e": "Queue the saved plan for a separate installer daemon process.",
|
||||||
|
"i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914": "Queued daemon handoffs created from the admin UI or CLI.",
|
||||||
|
"i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac": "Queueing installer requests requires maintenance access.",
|
||||||
|
"i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245": "Recent supervised installer records and the current package-install lock.",
|
||||||
|
"i18n:govoplan-admin.registered_tenant_spaces.61e17d70": "Registered tenant spaces.",
|
||||||
|
"i18n:govoplan-admin.reload.cce71553": "Reload",
|
||||||
|
"i18n:govoplan-admin.remove.e963907d": "Remove",
|
||||||
|
"i18n:govoplan-admin.request.43c2e7a7": "Request:",
|
||||||
|
"i18n:govoplan-admin.request_approval.6245aea1": "Request approval",
|
||||||
|
"i18n:govoplan-admin.requested.c26bf60f": "Requested",
|
||||||
|
"i18n:govoplan-admin.requests.f7194e6a": "Requests",
|
||||||
|
"i18n:govoplan-admin.required_data.1b1c1b34": "Required data",
|
||||||
|
"i18n:govoplan-admin.required_means_the_definition_must_remain_presen.8462063c": "Required means the definition must remain present and system-controlled. It does not automatically assign users or grant permissions.",
|
||||||
|
"i18n:govoplan-admin.required.d5793988": "Required:",
|
||||||
|
"i18n:govoplan-admin.required.eed6bfb4": "Required",
|
||||||
|
"i18n:govoplan-admin.requires.a4fc9357": "Requires:",
|
||||||
|
"i18n:govoplan-admin.restart_commands.035997f9": "Restart commands",
|
||||||
|
"i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63": "Restart/reload required after package changes",
|
||||||
|
"i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195": "Restrict authenticated API access to maintenance operators",
|
||||||
|
"i18n:govoplan-admin.retention.c7199d9e": "Retention",
|
||||||
|
"i18n:govoplan-admin.retry_of.3a6bb304": "Retry of:",
|
||||||
|
"i18n:govoplan-admin.retry.9f5cd8a2": "Retry",
|
||||||
|
"i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1": "Reusable encrypted SMTP/IMAP profiles and mail policy.",
|
||||||
|
"i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43": "Reusable file server connections, credentials and policy.",
|
||||||
|
"i18n:govoplan-admin.role.c3f104d1": "Role",
|
||||||
|
"i18n:govoplan-admin.roles.47dcc27d": "Roles",
|
||||||
|
"i18n:govoplan-admin.rollback_value.47e9c00b": "Rollback: {value0}",
|
||||||
|
"i18n:govoplan-admin.run_migrations.db6e0ce2": "Run migrations",
|
||||||
|
"i18n:govoplan-admin.running_registry.5274b24b": "Running registry",
|
||||||
|
"i18n:govoplan-admin.runtime_differs.43d1fd78": "Runtime differs",
|
||||||
|
"i18n:govoplan-admin.runtime_matches.a84afa48": "Runtime matches",
|
||||||
|
"i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509": "Safety-controlled configuration requests, approvals and rollback history.",
|
||||||
|
"i18n:govoplan-admin.save_plan.842dc280": "Save plan",
|
||||||
|
"i18n:govoplan-admin.save_settings.913aba9f": "Save settings",
|
||||||
|
"i18n:govoplan-admin.save_template.0885fab2": "Save template",
|
||||||
|
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save tenant role",
|
||||||
|
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Save the install plan before queueing a daemon request.",
|
||||||
|
"i18n:govoplan-admin.saved.c0ae8f6e": "Saved",
|
||||||
|
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
|
||||||
|
"i18n:govoplan-admin.saving.ae7e8875": "Saving...",
|
||||||
|
"i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54": "Scoped automation credentials capped by owner permissions.",
|
||||||
|
"i18n:govoplan-admin.secret.f4e7a874": "Secret",
|
||||||
|
"i18n:govoplan-admin.seq_value.0ae5fa47": "Seq {value0}",
|
||||||
|
"i18n:govoplan-admin.setting.fb449f71": "Setting",
|
||||||
|
"i18n:govoplan-admin.severity.de314fa0": "Severity",
|
||||||
|
"i18n:govoplan-admin.signed_and_trusted.bee31302": "Signed and trusted",
|
||||||
|
"i18n:govoplan-admin.signed_untrusted.864ed50c": "Signed, untrusted",
|
||||||
|
"i18n:govoplan-admin.signed.6e3665d8": "Signed",
|
||||||
|
"i18n:govoplan-admin.slug.094da9b9": "Slug",
|
||||||
|
"i18n:govoplan-admin.started.fe3824e9": "Started:",
|
||||||
|
"i18n:govoplan-admin.startup_state.d1300be6": "Startup state",
|
||||||
|
"i18n:govoplan-admin.status.bae7d5be": "Status",
|
||||||
|
"i18n:govoplan-admin.summary.12b71c3e": "Summary",
|
||||||
|
"i18n:govoplan-admin.supervisor_value.d9f0c8eb": "Supervisor: {value0}",
|
||||||
|
"i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92": "Supplied data JSON must be an object.",
|
||||||
|
"i18n:govoplan-admin.supplied_data_json.6932bfb7": "Supplied data JSON",
|
||||||
|
"i18n:govoplan-admin.system_administration.b151acea": "System administration",
|
||||||
|
"i18n:govoplan-admin.system_general_settings.7cd662ed": "System general settings",
|
||||||
|
"i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9": "System-governed group definitions.",
|
||||||
|
"i18n:govoplan-admin.system_level_administrative_history.49f76723": "System-level administrative history.",
|
||||||
|
"i18n:govoplan-admin.system_modules.629539f2": "System modules",
|
||||||
|
"i18n:govoplan-admin.system_roles.a9461aa6": "System roles",
|
||||||
|
"i18n:govoplan-admin.system_settings_saved.dac4f17b": "System settings saved.",
|
||||||
|
"i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499": "System-wide governance and tenant-local access management, separated by scope and enforced by the backend.",
|
||||||
|
"i18n:govoplan-admin.system.bc0792d8": "System",
|
||||||
|
"i18n:govoplan-admin.target.61ad50a9": "Target",
|
||||||
|
"i18n:govoplan-admin.template_details.d5d75e4d": "Template details",
|
||||||
|
"i18n:govoplan-admin.tenant_administration_capabilities.5d265972": "Tenant administration capabilities",
|
||||||
|
"i18n:govoplan-admin.tenant_administration.ffab51f1": "Tenant administration",
|
||||||
|
"i18n:govoplan-admin.tenant_availability.067a4f31": "Tenant availability",
|
||||||
|
"i18n:govoplan-admin.tenant_id.59eba244": "Tenant id",
|
||||||
|
"i18n:govoplan-admin.tenant_level_administrative_history_only.55495c3c": "Tenant-level administrative history only.",
|
||||||
|
"i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d": "Tenant-level privacy retention limits inherited by owned objects.",
|
||||||
|
"i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5": "Tenant-local and centrally managed groups.",
|
||||||
|
"i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2": "Tenant-local and centrally managed roles.",
|
||||||
|
"i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b": "Tenant locale and tenant-specific settings.",
|
||||||
|
"i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6": "Tenant memberships and inherited roles.",
|
||||||
|
"i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea": "Tenant permission bundles and system-managed role copies.",
|
||||||
|
"i18n:govoplan-admin.tenant_permissions.246294bc": "Tenant permissions",
|
||||||
|
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role",
|
||||||
|
"i18n:govoplan-admin.tenant_roles.51aca82d": "Tenant roles",
|
||||||
|
"i18n:govoplan-admin.tenant_users.cb800b38": "Tenant users",
|
||||||
|
"i18n:govoplan-admin.tenants.1f7ae776": "Tenants",
|
||||||
|
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "These settings are enforced by the backend. Central groups and tenant roles remain available even when local creation is disabled.",
|
||||||
|
"i18n:govoplan-admin.trace.04a75036": "Trace:",
|
||||||
|
"i18n:govoplan-admin.trusted.99f7ed54": "Trusted",
|
||||||
|
"i18n:govoplan-admin.type.3deb7456": "Type",
|
||||||
|
"i18n:govoplan-admin.uninstall.a735da1d": "Uninstall",
|
||||||
|
"i18n:govoplan-admin.unlocked.b3da7025": "Unlocked",
|
||||||
|
"i18n:govoplan-admin.unsigned.e91344ea": "Unsigned",
|
||||||
|
"i18n:govoplan-admin.untrusted.cdc7838a": "Untrusted",
|
||||||
|
"i18n:govoplan-admin.update_enabled_modules.9b7ae790": "Update enabled modules",
|
||||||
|
"i18n:govoplan-admin.updated.f2f8570d": "Updated",
|
||||||
|
"i18n:govoplan-admin.user_file_connector_policy_limits": "File connector policy limits for user-owned spaces.",
|
||||||
|
"i18n:govoplan-admin.user_mail_server_policy_limits": "Mail server policy limits for user-owned work.",
|
||||||
|
"i18n:govoplan-admin.user_retention_policy_limits": "Retention limits for user-owned records.",
|
||||||
|
"i18n:govoplan-admin.users.57f2b181": "Users",
|
||||||
|
"i18n:govoplan-admin.valid_after.c615c873": "· Valid after:",
|
||||||
|
"i18n:govoplan-admin.valid_and_trusted.73cf89bc": "Valid and trusted",
|
||||||
|
"i18n:govoplan-admin.valid.a4aefa35": "Valid",
|
||||||
|
"i18n:govoplan-admin.valid.b374b8f9": "Valid:",
|
||||||
|
"i18n:govoplan-admin.value_deleted.3c4bf574": "{value0} deleted.",
|
||||||
|
"i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149": "{value0} has a WebUI reference but no WebUI package.",
|
||||||
|
"i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75": "{value0} needs a Python package name for rollback.",
|
||||||
|
"i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa": "{value0} needs a Python package name for uninstall.",
|
||||||
|
"i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e": "{value0} needs a Python package reference.",
|
||||||
|
"i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073": "{value0} needs both WebUI package and WebUI reference, or neither.",
|
||||||
|
"i18n:govoplan-admin.value_template_created.d68a5166": "{value0} template created.",
|
||||||
|
"i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2": "{value0} updated and synchronized to assigned tenants.",
|
||||||
|
"i18n:govoplan-admin.value_value.c189e8bc": "{value0} ({value1})",
|
||||||
|
"i18n:govoplan-admin.version.2da600bf": "Version",
|
||||||
|
"i18n:govoplan-admin.waiting_for_maintenance.60d00019": "Waiting for maintenance",
|
||||||
|
"i18n:govoplan-admin.webui_package.19645536": "WebUI package",
|
||||||
|
"i18n:govoplan-admin.webui_rebuild_required.010ce49f": "WebUI rebuild required",
|
||||||
|
"i18n:govoplan-admin.webui_ref.cbae3559": "WebUI ref",
|
||||||
|
"i18n:govoplan-admin.will_disable.14bb193a": "Will disable",
|
||||||
|
"i18n:govoplan-admin.will_enable.5d52d70b": "Will enable",
|
||||||
|
"i18n:govoplan-admin.working.049ac820": "Working..."
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"i18n:govoplan-admin.access_configuration_exported.098f200d": "Access configuration exported.",
|
||||||
|
"i18n:govoplan-admin.action.97c89a4d": "Action",
|
||||||
|
"i18n:govoplan-admin.actions.c3cd636a": "Aktionen",
|
||||||
|
"i18n:govoplan-admin.activate_installs.731d22a1": "Installationen aktivieren",
|
||||||
|
"i18n:govoplan-admin.active_and_total_memberships.c0c20f10": "Active and total memberships.",
|
||||||
|
"i18n:govoplan-admin.active_tenant_automation_credentials.240c659e": "Active tenant automation credentials.",
|
||||||
|
"i18n:govoplan-admin.active_tenant.dfadf0aa": "Active tenant:",
|
||||||
|
"i18n:govoplan-admin.active.a733b809": "Aktiv",
|
||||||
|
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
|
||||||
|
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
|
||||||
|
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add tenant role",
|
||||||
|
"i18n:govoplan-admin.admin.4e7afebc": "Administration",
|
||||||
|
"i18n:govoplan-admin.administration.b8be3d12": "Administration",
|
||||||
|
"i18n:govoplan-admin.platform_administration": "Plattformadministration",
|
||||||
|
"i18n:govoplan-admin.alert_warning.804f617a": " alert warning",
|
||||||
|
"i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17": "Mandanten-API-Schlüssel standardmäßig erlauben",
|
||||||
|
"i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a": "Mandantendefinierte Gruppen standardmäßig erlauben",
|
||||||
|
"i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9": "Mandantendefinierte Rollen standardmäßig erlauben",
|
||||||
|
"i18n:govoplan-admin.api_keys.94fcf3c2": "API-Schlüssel",
|
||||||
|
"i18n:govoplan-admin.applied.a3e4a569": "Angewendet",
|
||||||
|
"i18n:govoplan-admin.apply_approved_request.ab218d9a": "Apply approved request",
|
||||||
|
"i18n:govoplan-admin.apply_finished_with_blockers.78487a12": "Apply finished with blockers.",
|
||||||
|
"i18n:govoplan-admin.apply_result.0fde1c3c": "Apply result",
|
||||||
|
"i18n:govoplan-admin.apply.cfea419c": "Anwenden",
|
||||||
|
"i18n:govoplan-admin.approvals.deb9d03c": "Approvals",
|
||||||
|
"i18n:govoplan-admin.approve.7b2c7f14": "Freigeben",
|
||||||
|
"i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16": "Approved package references that can be added to the operator install plan.",
|
||||||
|
"i18n:govoplan-admin.approved_value.52da808b": "Approved {value0}.",
|
||||||
|
"i18n:govoplan-admin.approvers.0e2de1fb": "Approvers",
|
||||||
|
"i18n:govoplan-admin.audit.fa1703dd": "Audit",
|
||||||
|
"i18n:govoplan-admin.available.7c62a142": "Verfügbar",
|
||||||
|
"i18n:govoplan-admin.blocked.99613c74": "Blockiert",
|
||||||
|
"i18n:govoplan-admin.build_webui.ed5ef1fd": " --build-webui",
|
||||||
|
"i18n:govoplan-admin.build_webui.fe8ccad7": "WebUI bauen",
|
||||||
|
"i18n:govoplan-admin.bypass_allowed.4e347c27": "Bypass allowed",
|
||||||
|
"i18n:govoplan-admin.bypass_denied.ab987400": "Bypass denied",
|
||||||
|
"i18n:govoplan-admin.cached_from.d73c5b2a": "· cached from",
|
||||||
|
"i18n:govoplan-admin.cancel.77dfd213": "Abbrechen",
|
||||||
|
"i18n:govoplan-admin.cancelled.8c9dcfa8": "Cancelled:",
|
||||||
|
"i18n:govoplan-admin.case_clerk.b78a314a": "Case clerk",
|
||||||
|
"i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f": "Catalog install entry planned.",
|
||||||
|
"i18n:govoplan-admin.catalog.4a88d27b": "Katalog",
|
||||||
|
"i18n:govoplan-admin.central_groups.5c9b5b66": "Central groups",
|
||||||
|
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9": "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local.",
|
||||||
|
"i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1": "Centrally governed tenant roles and their availability across tenants.",
|
||||||
|
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3": "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.",
|
||||||
|
"i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b": "Centrally governed tenant roles.",
|
||||||
|
"i18n:govoplan-admin.change_request_created_value.4af6d3d2": "Change request created: {value0}",
|
||||||
|
"i18n:govoplan-admin.change_request_id.96ee3239": "Change request id",
|
||||||
|
"i18n:govoplan-admin.change_request_ready.b17e8c57": "Change request ready:",
|
||||||
|
"i18n:govoplan-admin.changes.8aa57de6": "Änderungen",
|
||||||
|
"i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed": "Changing enabled modules requires a dry-run change request and active maintenance mode.",
|
||||||
|
"i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f": "Changing the maintenance-mode flag requires system:maintenance:access. Login remains available so an operator can sign in during maintenance.",
|
||||||
|
"i18n:govoplan-admin.channel_value.d3894efe": "Channel: {value0}",
|
||||||
|
"i18n:govoplan-admin.clear.719ea396": "Leeren",
|
||||||
|
"i18n:govoplan-admin.close.bbfa773e": "Schließen",
|
||||||
|
"i18n:govoplan-admin.code.adac6937": "Code",
|
||||||
|
"i18n:govoplan-admin.commands.f6a34aed": "Commands:",
|
||||||
|
"i18n:govoplan-admin.configuration_changes.82933bbb": "Konfigurationsänderungen",
|
||||||
|
"i18n:govoplan-admin.configuration_package_apply.c270e5f3": "Configuration package apply",
|
||||||
|
"i18n:govoplan-admin.configuration_packages.eb2f05f1": "Konfigurationspakete",
|
||||||
|
"i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335": "Configuration requests, approvals and version history.",
|
||||||
|
"i18n:govoplan-admin.configured_license.3e73f8f5": "Configured license",
|
||||||
|
"i18n:govoplan-admin.create_dry_run_request.05b9cbdb": "Create dry-run request",
|
||||||
|
"i18n:govoplan-admin.create_group_template.72407248": "Create group template",
|
||||||
|
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply",
|
||||||
|
"i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39": "Create, suspend and govern tenant spaces.",
|
||||||
|
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create tenant role",
|
||||||
|
"i18n:govoplan-admin.created.0c78dab1": "Created:",
|
||||||
|
"i18n:govoplan-admin.created.accf40c8": "Erstellt",
|
||||||
|
"i18n:govoplan-admin.daemon_execution.cc0fad8d": "Daemon-Ausführung",
|
||||||
|
"i18n:govoplan-admin.daemon_offline.314f19a4": "Daemon offline",
|
||||||
|
"i18n:govoplan-admin.daemon_value.e54f5a31": "Daemon: {value0}",
|
||||||
|
"i18n:govoplan-admin.db_backup_command.5bded3ac": "DB-Backup-Befehl",
|
||||||
|
"i18n:govoplan-admin.db_restore_check_command.750ae936": "DB-Wiederherstellungsprüfung",
|
||||||
|
"i18n:govoplan-admin.db_restore_command.de11eb01": "DB-Wiederherstellungsbefehl",
|
||||||
|
"i18n:govoplan-admin.db.e355c23a": "DB:",
|
||||||
|
"i18n:govoplan-admin.default_locale.b99d021f": "Standardsprache",
|
||||||
|
"i18n:govoplan-admin.install_language_package": "Sprachpaket installieren",
|
||||||
|
"i18n:govoplan-admin.language_code": "Sprachcode",
|
||||||
|
"i18n:govoplan-admin.language_name": "Sprachname",
|
||||||
|
"i18n:govoplan-admin.language_name_placeholder": "Franzoesisch",
|
||||||
|
"i18n:govoplan-admin.language_packages": "Sprachpakete",
|
||||||
|
"i18n:govoplan-admin.language_packages_help": "Installierte Pakete legen fest, welche Sprachcodes aktiviert werden koennen. Ein Paket ist erst vollstaendig uebersetzt, wenn passende Uebersetzungskataloge der Module vorhanden sind.",
|
||||||
|
"i18n:govoplan-admin.native_language_name": "Nativer Sprachname",
|
||||||
|
"i18n:govoplan-admin.native_language_name_placeholder": "Francais",
|
||||||
|
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten",
|
||||||
|
"i18n:govoplan-admin.delete_group_template.8745d842": "Delete group template",
|
||||||
|
"i18n:govoplan-admin.delete_template.399bf72a": "Delete template",
|
||||||
|
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete tenant role",
|
||||||
|
"i18n:govoplan-admin.delete_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.dependents.072665c4": "Dependents:",
|
||||||
|
"i18n:govoplan-admin.description.55f8ebc8": "Beschreibung",
|
||||||
|
"i18n:govoplan-admin.destroy_data.34557c3c": "Daten zerstören",
|
||||||
|
"i18n:govoplan-admin.disabled.f4f4473d": "Disabled",
|
||||||
|
"i18n:govoplan-admin.discovered_packages.660902de": "Discovered packages",
|
||||||
|
"i18n:govoplan-admin.drift.4876f7b9": "Drift",
|
||||||
|
"i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b": "Deinstallationen aus dem Start entfernen",
|
||||||
|
"i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07": "Dry-run change request created: {value0}. Enable maintenance mode, then apply the request.",
|
||||||
|
"i18n:govoplan-admin.dry_run.3d14659c": "Dry-run",
|
||||||
|
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template",
|
||||||
|
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit tenant role",
|
||||||
|
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
|
||||||
|
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
|
||||||
|
"i18n:govoplan-admin.enabled.df174a3f": "Aktiviert",
|
||||||
|
"i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968": "Enabling maintenance mode requires system:maintenance:access.",
|
||||||
|
"i18n:govoplan-admin.enabling.2b8e03e6": "Enabling...",
|
||||||
|
"i18n:govoplan-admin.enforced.93ff909c": "Erzwungen",
|
||||||
|
"i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206": "Every plan item needs a module id.",
|
||||||
|
"i18n:govoplan-admin.expires.2d21b7de": "· Expires:",
|
||||||
|
"i18n:govoplan-admin.export_access.9a2eb91e": "Export access",
|
||||||
|
"i18n:govoplan-admin.export_finished_with_blockers.e2f70611": "Export finished with blockers.",
|
||||||
|
"i18n:govoplan-admin.export.f3e4fadb": "Export",
|
||||||
|
"i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c": "Exported system access configuration",
|
||||||
|
"i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c": "Exported tenant access configuration",
|
||||||
|
"i18n:govoplan-admin.features.5df81ffa": "Features:",
|
||||||
|
"i18n:govoplan-admin.file_connections.1e362326": "Dateiverbindungen",
|
||||||
|
"i18n:govoplan-admin.finished.4b52fe3f": "Finished:",
|
||||||
|
"i18n:govoplan-admin.fragment.3f19d616": "Fragment",
|
||||||
|
"i18n:govoplan-admin.front_office.d9dcfee1": "Front office",
|
||||||
|
"i18n:govoplan-admin.general.9239ee2c": "Allgemein",
|
||||||
|
"i18n:govoplan-admin.generated.a2edf57c": "Generated:",
|
||||||
|
"i18n:govoplan-admin.global": "Global",
|
||||||
|
"i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f": "Global accounts, tenant memberships and system-role assignments.",
|
||||||
|
"i18n:govoplan-admin.global_login_accounts_across_all_tenants.2aab129d": "Global login accounts across all tenants.",
|
||||||
|
"i18n:govoplan-admin.govoplan_module_installer_format_shell.226d1450": "govoplan-module-installer --format shell",
|
||||||
|
"i18n:govoplan-admin.group_definitions_made_available_or_required_in_.55402e16": "Group definitions made available or required in selected tenants.",
|
||||||
|
"i18n:govoplan-admin.group_file_connector_policy_limits": "Dateiverbindungsrichtlinien fuer gruppeneigene Bereiche.",
|
||||||
|
"i18n:govoplan-admin.group_mail_server_policy_limits": "Mailserver-Richtlinien fuer gruppeneigene Arbeit.",
|
||||||
|
"i18n:govoplan-admin.group_retention_policy_limits": "Aufbewahrungslimits fuer gruppeneigene Datensaetze.",
|
||||||
|
"i18n:govoplan-admin.group_template.973e0fa6": "Group template",
|
||||||
|
"i18n:govoplan-admin.group.171a0606": "Group",
|
||||||
|
"i18n:govoplan-admin.groups.ae9629f4": "Gruppen",
|
||||||
|
"i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349": "Handles incoming administrative work.",
|
||||||
|
"i18n:govoplan-admin.health_urls.3b86647c": "Health-URLs",
|
||||||
|
"i18n:govoplan-admin.history.90ccd649": "History",
|
||||||
|
"i18n:govoplan-admin.id.474ae526": "Id",
|
||||||
|
"i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929": "Modul-eigene Konfigurationspakete importieren, prüfen, freigeben, anwenden und exportieren.",
|
||||||
|
"i18n:govoplan-admin.inactive.09af574c": "Inaktiv",
|
||||||
|
"i18n:govoplan-admin.inspect_value.9d5d1071": "Inspect {value0}",
|
||||||
|
"i18n:govoplan-admin.install.fd6c3ebf": "Installieren",
|
||||||
|
"i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd": "Installed modules, active runtime state, and saved startup state.",
|
||||||
|
"i18n:govoplan-admin.installed_modules_runtime_state_and_startup_stat.38dd7028": "Installed modules, runtime state and startup state.",
|
||||||
|
"i18n:govoplan-admin.installed.7bb4405c": "Installed",
|
||||||
|
"i18n:govoplan-admin.installer_preflight_blocked.cf83bf79": "Installer preflight blocked",
|
||||||
|
"i18n:govoplan-admin.installer_preflight_passed.6d5f8060": "Installer preflight passed",
|
||||||
|
"i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d": "Installer request cancelled: {value0}",
|
||||||
|
"i18n:govoplan-admin.installer_request_queued_value.00726abb": "Installer request queued: {value0}",
|
||||||
|
"i18n:govoplan-admin.installer_requests.b88db439": "Installer-Anfragen",
|
||||||
|
"i18n:govoplan-admin.installer_runs.e9e344e1": "Installer-Läufe",
|
||||||
|
"i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa": "Instance defaults and tenant governance capabilities.",
|
||||||
|
"i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069": "Instance privacy retention policy and lower-level override permissions.",
|
||||||
|
"i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97": "Instanzweite Standards und Governance-Funktionen für Mandanten. Die Aufbewahrungsrichtlinien werden in einem eigenen Systembereich verwaltet.",
|
||||||
|
"i18n:govoplan-admin.instance_wide_roles_assigned_directly_to_global_.91050488": "Instance-wide roles assigned directly to global accounts.",
|
||||||
|
"i18n:govoplan-admin.invalid_json.01ccb74f": "Invalid JSON.",
|
||||||
|
"i18n:govoplan-admin.invalid_license.f16bf749": "Invalid license",
|
||||||
|
"i18n:govoplan-admin.json_must_be_an_object.848569c9": "JSON must be an object.",
|
||||||
|
"i18n:govoplan-admin.key_value.847b914f": "Key: {value0}",
|
||||||
|
"i18n:govoplan-admin.key.c67dd20e": "Key",
|
||||||
|
"i18n:govoplan-admin.kind.e00ac23f": "Kind",
|
||||||
|
"i18n:govoplan-admin.label.74341e3c": "Label",
|
||||||
|
"i18n:govoplan-admin.last_request.4508ef35": "Last request:",
|
||||||
|
"i18n:govoplan-admin.license.de13bf1a": "License:",
|
||||||
|
"i18n:govoplan-admin.locked.a798882f": "Gesperrt",
|
||||||
|
"i18n:govoplan-admin.mail_servers.d627326a": "Mail servers",
|
||||||
|
"i18n:govoplan-admin.maintenance_message.ca62571f": "Wartungsmeldung",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c": "Maintenance mode enabled. Apply the module-state request when ready.",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629": "Maintenance mode is off. Package changes should be applied only after enabling maintenance mode.",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f": "Der Wartungsmodus muss aktiviert sein, bevor eine Daemon-Anfrage eingereiht wird.",
|
||||||
|
"i18n:govoplan-admin.maintenance_mode.98cca5c6": "Wartungsmodus",
|
||||||
|
"i18n:govoplan-admin.maintenance.94de303b": "Wartung",
|
||||||
|
"i18n:govoplan-admin.membership_status_groups_and_direct_roles_in_the.ab6c10b6": "Membership status, groups and direct roles in the active tenant.",
|
||||||
|
"i18n:govoplan-admin.message.68f4145f": "Nachricht",
|
||||||
|
"i18n:govoplan-admin.minimal_office_access.af48f49a": "Minimal office access",
|
||||||
|
"i18n:govoplan-admin.missing_entitlement.0cafa611": "Missing entitlement",
|
||||||
|
"i18n:govoplan-admin.missing.feb2bbaa": "Missing:",
|
||||||
|
"i18n:govoplan-admin.module_install_plan_cleared.afa2698d": "Module install plan cleared.",
|
||||||
|
"i18n:govoplan-admin.module_install_plan_saved.5d43d944": "Module install plan saved.",
|
||||||
|
"i18n:govoplan-admin.module_maintenance_in_progress.f6395152": "Module maintenance in progress.",
|
||||||
|
"i18n:govoplan-admin.module_state_saved.e3e22105": "Module state saved.",
|
||||||
|
"i18n:govoplan-admin.module.b8ff0289": "Modul",
|
||||||
|
"i18n:govoplan-admin.modules.04e9462c": "Module",
|
||||||
|
"i18n:govoplan-admin.modules.2dc3b299": "Modules:",
|
||||||
|
"i18n:govoplan-admin.name.709a2322": "Name",
|
||||||
|
"i18n:govoplan-admin.needs_attention.a126722e": "Benötigt Aufmerksamkeit",
|
||||||
|
"i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7": "No applied configuration changes.",
|
||||||
|
"i18n:govoplan-admin.no_central_value_templates_found.081149fa": "No central {value0} templates found.",
|
||||||
|
"i18n:govoplan-admin.no_diagnostics.2b6e2630": "No diagnostics.",
|
||||||
|
"i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa": "Noch keine Installer-Anfragen aufgezeichnet.",
|
||||||
|
"i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964": "Noch keine Installer-Läufe aufgezeichnet.",
|
||||||
|
"i18n:govoplan-admin.no_license_configured.693cea1a": "No license configured",
|
||||||
|
"i18n:govoplan-admin.no_license.eb4bfe19": "No license",
|
||||||
|
"i18n:govoplan-admin.no_open_requests.580c95f9": "No open requests.",
|
||||||
|
"i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3": "No package catalog configured. Set GOVOPLAN_MODULE_PACKAGE_CATALOG or GOVOPLAN_MODULE_PACKAGE_CATALOG_URL to enable curated install entries.",
|
||||||
|
"i18n:govoplan-admin.no_package_changes_planned.f12d8b33": "No package changes planned.",
|
||||||
|
"i18n:govoplan-admin.no_package_restart_pending.efe6b8ce": "No package restart pending",
|
||||||
|
"i18n:govoplan-admin.no_plan_items.7108c582": "No plan items.",
|
||||||
|
"i18n:govoplan-admin.none.6eef6648": "Keine",
|
||||||
|
"i18n:govoplan-admin.not_available.d1a17af1": "Not available",
|
||||||
|
"i18n:govoplan-admin.not_configured.811931bb": "Nicht konfiguriert",
|
||||||
|
"i18n:govoplan-admin.not_recorded.9925ee3c": "not recorded",
|
||||||
|
"i18n:govoplan-admin.notes.70440046": "Notes",
|
||||||
|
"i18n:govoplan-admin.object.2883f191": "Object",
|
||||||
|
"i18n:govoplan-admin.observe_only.c6a1f8bc": "Nur beobachten",
|
||||||
|
"i18n:govoplan-admin.off.e3de5ab0": "Off",
|
||||||
|
"i18n:govoplan-admin.on.e0049a66": "On",
|
||||||
|
"i18n:govoplan-admin.operator_install_plan.b203aabc": "Operator install plan",
|
||||||
|
"i18n:govoplan-admin.optional.166b8369": "Optional:",
|
||||||
|
"i18n:govoplan-admin.overview.0efc2e6b": "Übersicht",
|
||||||
|
"i18n:govoplan-admin.owner.89ff3122": "Eigentümer",
|
||||||
|
"i18n:govoplan-admin.package_applied.63782ce7": "Package applied.",
|
||||||
|
"i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb": "Package catalog is configured but contains no entries.",
|
||||||
|
"i18n:govoplan-admin.package_catalog.b36e6d31": "Package catalog",
|
||||||
|
"i18n:govoplan-admin.package_json_must_be_an_object.db825bb3": "Package JSON must be an object.",
|
||||||
|
"i18n:govoplan-admin.package_json.a2b10f38": "Package JSON",
|
||||||
|
"i18n:govoplan-admin.package_uninstall_planned.56e8566d": "Package uninstall planned.",
|
||||||
|
"i18n:govoplan-admin.package.7431e3df": "Package",
|
||||||
|
"i18n:govoplan-admin.packages.0a999012": "Pakete",
|
||||||
|
"i18n:govoplan-admin.pending.c515ec74": " pending",
|
||||||
|
"i18n:govoplan-admin.permissions.d06d5557": "Berechtigungen",
|
||||||
|
"i18n:govoplan-admin.plan_install.e82bffe6": "Plan install",
|
||||||
|
"i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf": "Plan package installs and removals here, then apply the rendered commands from an operator shell during maintenance mode.",
|
||||||
|
"i18n:govoplan-admin.plan_uninstall.e62804ab": "Plan uninstall",
|
||||||
|
"i18n:govoplan-admin.plan.ae2f98a0": "Plan",
|
||||||
|
"i18n:govoplan-admin.planned.9cbe42aa": "Geplant",
|
||||||
|
"i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8": "Preflight, approve, apply and export configuration packages.",
|
||||||
|
"i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b": "Preflight finished with blockers.",
|
||||||
|
"i18n:govoplan-admin.preflight_passed.c0c99055": "Preflight passed.",
|
||||||
|
"i18n:govoplan-admin.preflight.8016a487": "Preflight",
|
||||||
|
"i18n:govoplan-admin.python_package.34591c71": "Python-Paket",
|
||||||
|
"i18n:govoplan-admin.python_ref.1af35d0d": "Python-Referenz",
|
||||||
|
"i18n:govoplan-admin.queue_supervised_run.a53f248c": "Queue supervised run",
|
||||||
|
"i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e": "Den gespeicherten Plan für einen separaten Installer-Daemon-Prozess einreihen.",
|
||||||
|
"i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914": "Queued daemon handoffs created from the admin UI or CLI.",
|
||||||
|
"i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac": "Das Einreihen von Installer-Anfragen erfordert Wartungszugriff.",
|
||||||
|
"i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245": "Aktuelle überwachte Installer-Datensätze und die aktuelle Paketinstallationssperre.",
|
||||||
|
"i18n:govoplan-admin.registered_tenant_spaces.61e17d70": "Registered tenant spaces.",
|
||||||
|
"i18n:govoplan-admin.reload.cce71553": "Neu laden",
|
||||||
|
"i18n:govoplan-admin.remove.e963907d": "Entfernen",
|
||||||
|
"i18n:govoplan-admin.request.43c2e7a7": "Anfrage:",
|
||||||
|
"i18n:govoplan-admin.request_approval.6245aea1": "Request approval",
|
||||||
|
"i18n:govoplan-admin.requested.c26bf60f": "Requested",
|
||||||
|
"i18n:govoplan-admin.requests.f7194e6a": "Requests",
|
||||||
|
"i18n:govoplan-admin.required_data.1b1c1b34": "Required data",
|
||||||
|
"i18n:govoplan-admin.required_means_the_definition_must_remain_presen.8462063c": "Required means the definition must remain present and system-controlled. It does not automatically assign users or grant permissions.",
|
||||||
|
"i18n:govoplan-admin.required.d5793988": "Required:",
|
||||||
|
"i18n:govoplan-admin.required.eed6bfb4": "Required",
|
||||||
|
"i18n:govoplan-admin.requires.a4fc9357": "Requires:",
|
||||||
|
"i18n:govoplan-admin.restart_commands.035997f9": "Neustartbefehle",
|
||||||
|
"i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63": "Restart/reload required after package changes",
|
||||||
|
"i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195": "Authentifizierten API-Zugriff auf Wartungsoperatoren beschränken",
|
||||||
|
"i18n:govoplan-admin.retention.c7199d9e": "Retention",
|
||||||
|
"i18n:govoplan-admin.retry_of.3a6bb304": "Retry of:",
|
||||||
|
"i18n:govoplan-admin.retry.9f5cd8a2": "Erneut versuchen",
|
||||||
|
"i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1": "Reusable encrypted SMTP/IMAP profiles and mail policy.",
|
||||||
|
"i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43": "Reusable file server connections, credentials and policy.",
|
||||||
|
"i18n:govoplan-admin.role.c3f104d1": "Role",
|
||||||
|
"i18n:govoplan-admin.roles.47dcc27d": "Roles",
|
||||||
|
"i18n:govoplan-admin.rollback_value.47e9c00b": "Rollback: {value0}",
|
||||||
|
"i18n:govoplan-admin.run_migrations.db6e0ce2": "Migrationen ausführen",
|
||||||
|
"i18n:govoplan-admin.running_registry.5274b24b": "Running registry",
|
||||||
|
"i18n:govoplan-admin.runtime_differs.43d1fd78": "Runtime differs",
|
||||||
|
"i18n:govoplan-admin.runtime_matches.a84afa48": "Runtime matches",
|
||||||
|
"i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509": "Safety-controlled configuration requests, approvals and rollback history.",
|
||||||
|
"i18n:govoplan-admin.save_plan.842dc280": "Save plan",
|
||||||
|
"i18n:govoplan-admin.save_settings.913aba9f": "Einstellungen speichern",
|
||||||
|
"i18n:govoplan-admin.save_template.0885fab2": "Save template",
|
||||||
|
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save tenant role",
|
||||||
|
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Speichern Sie den Installationsplan, bevor eine Daemon-Anfrage eingereiht wird.",
|
||||||
|
"i18n:govoplan-admin.saved.c0ae8f6e": "Gespeichert",
|
||||||
|
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
|
||||||
|
"i18n:govoplan-admin.saving.ae7e8875": "Saving...",
|
||||||
|
"i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54": "Scoped automation credentials capped by owner permissions.",
|
||||||
|
"i18n:govoplan-admin.secret.f4e7a874": "Secret",
|
||||||
|
"i18n:govoplan-admin.seq_value.0ae5fa47": "Seq {value0}",
|
||||||
|
"i18n:govoplan-admin.setting.fb449f71": "Setting",
|
||||||
|
"i18n:govoplan-admin.severity.de314fa0": "Severity",
|
||||||
|
"i18n:govoplan-admin.signed_and_trusted.bee31302": "Signed and trusted",
|
||||||
|
"i18n:govoplan-admin.signed_untrusted.864ed50c": "Signiert, nicht vertrauenswürdig",
|
||||||
|
"i18n:govoplan-admin.signed.6e3665d8": "Signiert",
|
||||||
|
"i18n:govoplan-admin.slug.094da9b9": "Slug",
|
||||||
|
"i18n:govoplan-admin.started.fe3824e9": "Started:",
|
||||||
|
"i18n:govoplan-admin.startup_state.d1300be6": "Startup state",
|
||||||
|
"i18n:govoplan-admin.status.bae7d5be": "Status",
|
||||||
|
"i18n:govoplan-admin.summary.12b71c3e": "Summary",
|
||||||
|
"i18n:govoplan-admin.supervisor_value.d9f0c8eb": "Supervisor: {value0}",
|
||||||
|
"i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92": "Supplied data JSON must be an object.",
|
||||||
|
"i18n:govoplan-admin.supplied_data_json.6932bfb7": "Supplied data JSON",
|
||||||
|
"i18n:govoplan-admin.system_administration.b151acea": "System administration",
|
||||||
|
"i18n:govoplan-admin.system_general_settings.7cd662ed": "Allgemeine Systemeinstellungen",
|
||||||
|
"i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9": "System-governed group definitions.",
|
||||||
|
"i18n:govoplan-admin.system_level_administrative_history.49f76723": "System-level administrative history.",
|
||||||
|
"i18n:govoplan-admin.system_modules.629539f2": "System modules",
|
||||||
|
"i18n:govoplan-admin.system_roles.a9461aa6": "System roles",
|
||||||
|
"i18n:govoplan-admin.system_settings_saved.dac4f17b": "Systemeinstellungen gespeichert.",
|
||||||
|
"i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499": "System-wide governance and tenant-local access management, separated by scope and enforced by the backend.",
|
||||||
|
"i18n:govoplan-admin.system.bc0792d8": "System",
|
||||||
|
"i18n:govoplan-admin.target.61ad50a9": "Target",
|
||||||
|
"i18n:govoplan-admin.template_details.d5d75e4d": "Template details",
|
||||||
|
"i18n:govoplan-admin.tenant_administration_capabilities.5d265972": "Tenant administration capabilities",
|
||||||
|
"i18n:govoplan-admin.tenant_administration.ffab51f1": "Tenant administration",
|
||||||
|
"i18n:govoplan-admin.tenant_availability.067a4f31": "Tenant availability",
|
||||||
|
"i18n:govoplan-admin.tenant_id.59eba244": "Tenant id",
|
||||||
|
"i18n:govoplan-admin.tenant_level_administrative_history_only.55495c3c": "Tenant-level administrative history only.",
|
||||||
|
"i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d": "Tenant-level privacy retention limits inherited by owned objects.",
|
||||||
|
"i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5": "Tenant-local and centrally managed groups.",
|
||||||
|
"i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2": "Tenant-local and centrally managed roles.",
|
||||||
|
"i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b": "Tenant locale and tenant-specific settings.",
|
||||||
|
"i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6": "Tenant memberships and inherited roles.",
|
||||||
|
"i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea": "Tenant permission bundles and system-managed role copies.",
|
||||||
|
"i18n:govoplan-admin.tenant_permissions.246294bc": "Tenant permissions",
|
||||||
|
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role",
|
||||||
|
"i18n:govoplan-admin.tenant_roles.51aca82d": "Mandantenrollen",
|
||||||
|
"i18n:govoplan-admin.tenant_users.cb800b38": "Mandantenbenutzer",
|
||||||
|
"i18n:govoplan-admin.tenants.1f7ae776": "Mandanten",
|
||||||
|
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "Diese Einstellungen werden vom Backend erzwungen. Zentrale Gruppen und Mandantenrollen bleiben verfügbar, auch wenn lokale Erstellung deaktiviert ist.",
|
||||||
|
"i18n:govoplan-admin.trace.04a75036": "Trace:",
|
||||||
|
"i18n:govoplan-admin.trusted.99f7ed54": "Vertrauenswürdig",
|
||||||
|
"i18n:govoplan-admin.type.3deb7456": "Typ",
|
||||||
|
"i18n:govoplan-admin.uninstall.a735da1d": "Deinstallieren",
|
||||||
|
"i18n:govoplan-admin.unlocked.b3da7025": "Entsperrt",
|
||||||
|
"i18n:govoplan-admin.unsigned.e91344ea": "Unsigniert",
|
||||||
|
"i18n:govoplan-admin.untrusted.cdc7838a": "Nicht vertrauenswürdig",
|
||||||
|
"i18n:govoplan-admin.update_enabled_modules.9b7ae790": "Update enabled modules",
|
||||||
|
"i18n:govoplan-admin.updated.f2f8570d": "Aktualisiert",
|
||||||
|
"i18n:govoplan-admin.user_file_connector_policy_limits": "Dateiverbindungsrichtlinien fuer benutzereigene Bereiche.",
|
||||||
|
"i18n:govoplan-admin.user_mail_server_policy_limits": "Mailserver-Richtlinien fuer benutzereigene Arbeit.",
|
||||||
|
"i18n:govoplan-admin.user_retention_policy_limits": "Aufbewahrungslimits fuer benutzereigene Datensaetze.",
|
||||||
|
"i18n:govoplan-admin.users.57f2b181": "Benutzer",
|
||||||
|
"i18n:govoplan-admin.valid_after.c615c873": "· Valid after:",
|
||||||
|
"i18n:govoplan-admin.valid_and_trusted.73cf89bc": "Valid and trusted",
|
||||||
|
"i18n:govoplan-admin.valid.a4aefa35": "Gültig",
|
||||||
|
"i18n:govoplan-admin.valid.b374b8f9": "Valid:",
|
||||||
|
"i18n:govoplan-admin.value_deleted.3c4bf574": "{value0} deleted.",
|
||||||
|
"i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149": "{value0} has a WebUI reference but no WebUI package.",
|
||||||
|
"i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75": "{value0} needs a Python package name for rollback.",
|
||||||
|
"i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa": "{value0} needs a Python package name for uninstall.",
|
||||||
|
"i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e": "{value0} needs a Python package reference.",
|
||||||
|
"i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073": "{value0} needs both WebUI package and WebUI reference, or neither.",
|
||||||
|
"i18n:govoplan-admin.value_template_created.d68a5166": "{value0} template created.",
|
||||||
|
"i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2": "{value0} updated and synchronized to assigned tenants.",
|
||||||
|
"i18n:govoplan-admin.value_value.c189e8bc": "{value0} ({value1})",
|
||||||
|
"i18n:govoplan-admin.version.2da600bf": "Version",
|
||||||
|
"i18n:govoplan-admin.waiting_for_maintenance.60d00019": "Waiting for maintenance",
|
||||||
|
"i18n:govoplan-admin.webui_package.19645536": "WebUI-Paket",
|
||||||
|
"i18n:govoplan-admin.webui_rebuild_required.010ce49f": "WebUI rebuild required",
|
||||||
|
"i18n:govoplan-admin.webui_ref.cbae3559": "WebUI-Referenz",
|
||||||
|
"i18n:govoplan-admin.will_disable.14bb193a": "Wird deaktiviert",
|
||||||
|
"i18n:govoplan-admin.will_enable.5d52d70b": "Wird aktiviert",
|
||||||
|
"i18n:govoplan-admin.working.049ac820": "Working..."
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ export { default } from "./module";
|
|||||||
export * from "./module";
|
export * from "./module";
|
||||||
export * from "./api/admin";
|
export * from "./api/admin";
|
||||||
export { default as AdminOverviewPanel } from "./features/admin/AdminOverviewPanel";
|
export { default as AdminOverviewPanel } from "./features/admin/AdminOverviewPanel";
|
||||||
|
export { default as ConfigurationPackagesPanel } from "./features/admin/ConfigurationPackagesPanel";
|
||||||
export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel";
|
export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel";
|
||||||
export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel";
|
export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel";
|
||||||
export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui";
|
export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui";
|
||||||
|
|||||||
@@ -1,87 +1,118 @@
|
|||||||
import { createElement, lazy } from "react";
|
import { createElement, lazy } from "react";
|
||||||
import type { AdminSectionsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
import type { AdminSectionsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||||
import { adminReadScopes, hasScope } from "@govoplan/core-webui";
|
import { adminReadScopes, hasScope } from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
|
|
||||||
const AdminOverviewPanel = lazy(() => import("./features/admin/AdminOverviewPanel"));
|
const AdminOverviewPanel = lazy(() => import("./features/admin/AdminOverviewPanel"));
|
||||||
|
const ConfigurationChangesPanel = lazy(() => import("./features/admin/ConfigurationChangesPanel"));
|
||||||
|
const ConfigurationPackagesPanel = lazy(() => import("./features/admin/ConfigurationPackagesPanel"));
|
||||||
const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceTemplatesPanel"));
|
const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceTemplatesPanel"));
|
||||||
const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
|
const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
|
||||||
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
|
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: generatedTranslations.en,
|
||||||
|
de: generatedTranslations.de
|
||||||
|
};
|
||||||
|
|
||||||
const adminSections: AdminSectionsUiCapability = {
|
const adminSections: AdminSectionsUiCapability = {
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
id: "overview",
|
id: "overview",
|
||||||
label: "Overview",
|
label: "i18n:govoplan-admin.overview.0efc2e6b",
|
||||||
group: "ROOT",
|
group: "ROOT",
|
||||||
order: 0,
|
order: 0,
|
||||||
anyOf: adminReadScopes,
|
anyOf: adminReadScopes,
|
||||||
render: ({ settings, availableSections, selectSection }) => createElement(AdminOverviewPanel, {
|
render: ({ settings, availableSections, selectSection }) => createElement(AdminOverviewPanel, {
|
||||||
settings,
|
settings,
|
||||||
availableSections,
|
availableSections,
|
||||||
onSelect: selectSection
|
onSelect: selectSection
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-settings",
|
id: "system-settings",
|
||||||
label: "General",
|
label: "i18n:govoplan-admin.general.9239ee2c",
|
||||||
group: "SYSTEM",
|
group: "SYSTEM",
|
||||||
order: 10,
|
order: 10,
|
||||||
allOf: ["system:settings:read"],
|
allOf: ["system:settings:read"],
|
||||||
render: ({ settings, auth }) => createElement(SystemSettingsPanel, {
|
render: ({ settings, auth }) => createElement(SystemSettingsPanel, {
|
||||||
settings,
|
settings,
|
||||||
canWrite: hasScope(auth, "system:settings:write"),
|
canWrite: hasScope(auth, "system:settings:write"),
|
||||||
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
|
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system-role-templates",
|
id: "system-configuration-changes",
|
||||||
label: "Tenant roles",
|
label: "i18n:govoplan-admin.changes.8aa57de6",
|
||||||
group: "SYSTEM",
|
group: "SYSTEM",
|
||||||
order: 40,
|
order: 20,
|
||||||
allOf: ["system:governance:read"],
|
allOf: ["system:settings:read"],
|
||||||
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
|
render: ({ settings, auth }) => createElement(ConfigurationChangesPanel, {
|
||||||
settings,
|
settings,
|
||||||
kind: "role",
|
canApprove: hasScope(auth, "system:settings:write") || hasScope(auth, "system:governance:write")
|
||||||
canWrite: hasScope(auth, "system:governance:write"),
|
})
|
||||||
onAuthRefresh: refreshAuth
|
},
|
||||||
})
|
{
|
||||||
},
|
id: "system-configuration-packages",
|
||||||
{
|
label: "i18n:govoplan-admin.packages.0a999012",
|
||||||
id: "system-modules",
|
group: "SYSTEM",
|
||||||
label: "Modules",
|
order: 30,
|
||||||
group: "SYSTEM",
|
allOf: ["system:settings:read"],
|
||||||
order: 85,
|
render: ({ settings, auth }) => createElement(ConfigurationPackagesPanel, {
|
||||||
allOf: ["system:settings:read"],
|
settings,
|
||||||
render: ({ settings, auth }) => createElement(ModuleManagementPanel, {
|
canWrite: hasScope(auth, "system:settings:write")
|
||||||
settings,
|
})
|
||||||
canWrite: hasScope(auth, "system:settings:write"),
|
},
|
||||||
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
|
{
|
||||||
})
|
id: "system-role-templates",
|
||||||
},
|
label: "i18n:govoplan-admin.tenant_roles.51aca82d",
|
||||||
{
|
group: "SYSTEM",
|
||||||
id: "system-groups",
|
order: 40,
|
||||||
label: "Groups",
|
allOf: ["system:governance:read"],
|
||||||
group: "SYSTEM",
|
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
|
||||||
order: 50,
|
settings,
|
||||||
allOf: ["system:governance:read"],
|
kind: "role",
|
||||||
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
|
canWrite: hasScope(auth, "system:governance:write"),
|
||||||
settings,
|
onAuthRefresh: refreshAuth
|
||||||
kind: "group",
|
})
|
||||||
canWrite: hasScope(auth, "system:governance:write"),
|
},
|
||||||
onAuthRefresh: refreshAuth
|
{
|
||||||
})
|
id: "system-modules",
|
||||||
}
|
label: "i18n:govoplan-admin.modules.04e9462c",
|
||||||
]
|
group: "SYSTEM",
|
||||||
|
order: 85,
|
||||||
|
allOf: ["system:settings:read"],
|
||||||
|
render: ({ settings, auth }) => createElement(ModuleManagementPanel, {
|
||||||
|
settings,
|
||||||
|
canWrite: hasScope(auth, "system:settings:write"),
|
||||||
|
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-groups",
|
||||||
|
label: "i18n:govoplan-admin.groups.ae9629f4",
|
||||||
|
group: "SYSTEM",
|
||||||
|
order: 50,
|
||||||
|
allOf: ["system:governance:read"],
|
||||||
|
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
|
||||||
|
settings,
|
||||||
|
kind: "group",
|
||||||
|
canWrite: hasScope(auth, "system:governance:write"),
|
||||||
|
onAuthRefresh: refreshAuth
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const adminModule: PlatformWebModule = {
|
export const adminModule: PlatformWebModule = {
|
||||||
id: "admin",
|
id: "admin",
|
||||||
label: "Admin",
|
label: "i18n:govoplan-admin.admin.4e7afebc",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
dependencies: ["access"],
|
dependencies: ["access"],
|
||||||
|
translations,
|
||||||
uiCapabilities: {
|
uiCapabilities: {
|
||||||
"admin.sections": adminSections
|
"admin.sections": adminSections
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default adminModule;
|
export default adminModule;
|
||||||
Reference in New Issue
Block a user