Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57e2a34c89 | ||
|
|
e93630ab87 | ||
|
|
06a0c0d26c | ||
|
|
e890a90d08 | ||
|
|
60e2676809 | ||
|
|
b71523e364 | ||
|
|
d428f3390a | ||
|
|
6dbccd5e08 | ||
|
|
fef4e10afd | ||
|
|
8ee12c9aa8 | ||
|
|
f245603077 | ||
|
|
f3e69b97ee | ||
|
|
36291a57c1 | ||
|
|
e8a3e1c18f | ||
|
|
158b59dfb1 | ||
|
|
0fcd4dc06f | ||
|
|
e8cb9d4fb2 | ||
|
|
fcf93f438d | ||
|
|
f0ff4ee51d | ||
|
|
c9e1fb287f | ||
|
|
11ecf362a3 | ||
|
|
24edb7eb8a | ||
|
|
8c82d5b4f8 | ||
|
|
f362f3806b | ||
|
|
30a0281c66 | ||
|
|
970625edff | ||
|
|
0f2a9beca7 |
@@ -0,0 +1,270 @@
|
||||
name: Module Package Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Existing protected version tag to publish
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-packages:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Select and validate protected release tag
|
||||
shell: bash
|
||||
env:
|
||||
REQUESTED_TAG: ${{ inputs.release_tag }}
|
||||
TRIGGER_TAG: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${REQUESTED_TAG:-$TRIGGER_TAG}"
|
||||
case "$tag" in
|
||||
v[0-9]*.[0-9]*.[0-9]*) ;;
|
||||
*) echo "Release tag must start with a SemVer-shaped vX.Y.Z value" >&2; exit 1 ;;
|
||||
esac
|
||||
git fetch --force origin "refs/tags/$tag:refs/tags/$tag" refs/heads/main:refs/remotes/origin/main
|
||||
tag_commit="$(git rev-list -n 1 "$tag")"
|
||||
git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main || {
|
||||
echo "Release tag is not contained in main" >&2
|
||||
exit 1
|
||||
}
|
||||
git checkout --detach "$tag"
|
||||
printf 'RELEASE_TAG=%s\n' "$tag" >> "$GITEA_ENV"
|
||||
printf 'SOURCE_DATE_EPOCH=%s\n' "$(git show -s --format=%ct HEAD)" >> "$GITEA_ENV"
|
||||
- name: Validate package versions
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
tag = os.environ["RELEASE_TAG"]
|
||||
expected = tag.removeprefix("v")
|
||||
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
|
||||
if project.get("version") != expected:
|
||||
raise SystemExit(f"pyproject version {project.get('version')!r} does not match {tag}")
|
||||
if re.fullmatch(r"govoplan-[a-z0-9-]+", str(project.get("name", ""))) is None:
|
||||
raise SystemExit("Python distribution name must use the govoplan-* namespace")
|
||||
webui = Path("webui/package.json")
|
||||
if webui.is_file():
|
||||
package = json.loads(webui.read_text(encoding="utf-8"))
|
||||
if package.get("version") != expected:
|
||||
raise SystemExit(f"WebUI version {package.get('version')!r} does not match {tag}")
|
||||
if re.fullmatch(r"@govoplan/[a-z0-9-]+-webui", str(package.get("name", ""))) is None:
|
||||
raise SystemExit("WebUI package name must use the @govoplan/*-webui namespace")
|
||||
release = Path("webui/package.release.json")
|
||||
if release.is_file():
|
||||
release_package = json.loads(release.read_text(encoding="utf-8"))
|
||||
if (
|
||||
release_package.get("name") != package.get("name")
|
||||
or release_package.get("version") != expected
|
||||
):
|
||||
raise SystemExit("WebUI release package identity does not match package.json and the release tag")
|
||||
PY
|
||||
- name: Build immutable package artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
|
||||
rm -rf dist .package-webui
|
||||
python -m build --wheel --outdir dist
|
||||
python -m twine check dist/*.whl
|
||||
if [[ -f webui/package.json ]]; then
|
||||
mkdir .package-webui
|
||||
cp -a webui/. .package-webui/
|
||||
rm -rf .package-webui/node_modules .package-webui/dist
|
||||
if [[ -f .package-webui/package.release.json ]]; then
|
||||
cp .package-webui/package.release.json .package-webui/package.json
|
||||
fi
|
||||
node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = ".package-webui/package.json";
|
||||
const packageJson = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
const groups = ["dependencies", "optionalDependencies", "peerDependencies"];
|
||||
for (const group of groups) {
|
||||
for (const [name, specifier] of Object.entries(packageJson[group] || {})) {
|
||||
if (!name.startsWith("@govoplan/")) continue;
|
||||
if (typeof specifier !== "string") {
|
||||
throw new Error(`${group}.${name} must use a string version`);
|
||||
}
|
||||
const packageSlug = name.slice("@govoplan/".length);
|
||||
if (!packageSlug.endsWith("-webui")) {
|
||||
throw new Error(`${group}.${name} is outside the WebUI package namespace`);
|
||||
}
|
||||
const repository = `govoplan-${packageSlug.slice(0, -"-webui".length)}`;
|
||||
const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const gitTag = specifier.match(
|
||||
new RegExp(
|
||||
`^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/(?:GovOPlaN|add-ideas)/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`,
|
||||
),
|
||||
);
|
||||
if (gitTag) {
|
||||
packageJson[group][name] = gitTag[1];
|
||||
continue;
|
||||
}
|
||||
if (specifier.startsWith("file:") || specifier.startsWith("git+")) {
|
||||
throw new Error(
|
||||
`${group}.${name} must resolve to an exact registry version for publication`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
delete packageJson.private;
|
||||
fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
NODE
|
||||
npm pkg delete private --prefix .package-webui
|
||||
(cd .package-webui && npm pack --ignore-scripts --pack-destination ../dist)
|
||||
fi
|
||||
python - <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
artifacts = []
|
||||
for path in sorted(Path("dist").iterdir()):
|
||||
if path.suffix not in {".whl", ".tgz"}:
|
||||
continue
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
artifacts.append({"filename": path.name, "sha256": digest, "size": path.stat().st_size})
|
||||
payload = {
|
||||
"schema_version": "1",
|
||||
"repository": os.environ["GITEA_REPOSITORY"],
|
||||
"tag": os.environ["RELEASE_TAG"],
|
||||
"commit": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
Path("dist/package-artifacts.json").write_text(
|
||||
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
PY
|
||||
- name: Retain package hash evidence
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
|
||||
with:
|
||||
name: module-packages-${{ gitea.ref_name }}
|
||||
path: dist/package-artifacts.json
|
||||
- name: Check immutable registry state
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$PACKAGE_TOKEN"
|
||||
python - <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
api_root = "https://git.add-ideas.de/api/v1/packages/GovOPlaN"
|
||||
token = os.environ["PACKAGE_TOKEN"]
|
||||
|
||||
def should_publish(kind, name, version, path):
|
||||
package_url = "/".join(
|
||||
(api_root, kind, quote(name, safe=""), quote(version, safe=""), "files")
|
||||
)
|
||||
request = Request(
|
||||
package_url,
|
||||
headers={"Accept": "application/json", "Authorization": f"token {token}"},
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
files = json.load(response)
|
||||
except HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
print(f"{kind} package {name}=={version} is not published yet")
|
||||
return True
|
||||
raise
|
||||
if not isinstance(files, list) or len(files) != 1:
|
||||
raise SystemExit(
|
||||
f"immutable {kind} package {name}=={version} has an unexpected file set"
|
||||
)
|
||||
expected_sha256 = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
if files[0].get("sha256") != expected_sha256:
|
||||
raise SystemExit(
|
||||
f"immutable {kind} package {name}=={version} already exists with a different SHA-256"
|
||||
)
|
||||
print(f"verified existing {kind} package {name}=={version} ({expected_sha256})")
|
||||
return False
|
||||
|
||||
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
|
||||
wheels = tuple(Path("dist").glob("*.whl"))
|
||||
if len(wheels) != 1:
|
||||
raise SystemExit("release build must contain exactly one wheel")
|
||||
publish_pypi = should_publish(
|
||||
"pypi", str(project["name"]), str(project["version"]), wheels[0]
|
||||
)
|
||||
|
||||
tarballs = tuple(Path("dist").glob("*.tgz"))
|
||||
if len(tarballs) > 1:
|
||||
raise SystemExit("release build must contain at most one npm package")
|
||||
publish_npm = False
|
||||
if tarballs:
|
||||
webui = json.loads(
|
||||
Path(".package-webui/package.json").read_text(encoding="utf-8")
|
||||
)
|
||||
publish_npm = should_publish(
|
||||
"npm", str(webui["name"]), str(webui["version"]), tarballs[0]
|
||||
)
|
||||
|
||||
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
|
||||
env_file.write(f"PUBLISH_PYPI={int(publish_pypi)}\n")
|
||||
env_file.write(f"PUBLISH_NPM={int(publish_npm)}\n")
|
||||
PY
|
||||
- name: Publish wheel and WebUI package
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
|
||||
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$PACKAGE_USERNAME"
|
||||
test -n "$PACKAGE_TOKEN"
|
||||
if [[ "$PUBLISH_PYPI" == 1 ]]; then
|
||||
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
|
||||
python -m twine upload --non-interactive \
|
||||
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
|
||||
dist/*.whl
|
||||
else
|
||||
echo "Exact wheel is already present; skipping immutable retry."
|
||||
fi
|
||||
shopt -s nullglob
|
||||
webui_packages=(dist/*.tgz)
|
||||
if (( ${#webui_packages[@]} )) && [[ "$PUBLISH_NPM" == 1 ]]; then
|
||||
npmrc="$(mktemp)"
|
||||
trap 'rm -f "$npmrc"' EXIT
|
||||
chmod 600 "$npmrc"
|
||||
printf '%s\n' \
|
||||
'@govoplan:registry=https://git.add-ideas.de/api/packages/GovOPlaN/npm/' \
|
||||
"//git.add-ideas.de/api/packages/GovOPlaN/npm/:_authToken=$PACKAGE_TOKEN" \
|
||||
> "$npmrc"
|
||||
NPM_CONFIG_USERCONFIG="$npmrc" npm publish "./${webui_packages[0]}" \
|
||||
--ignore-scripts --access public \
|
||||
--registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/
|
||||
elif (( ${#webui_packages[@]} )); then
|
||||
echo "Exact WebUI package is already present; skipping immutable retry."
|
||||
fi
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
__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/
|
||||
|
||||
# GovOPlaN shared ignore rules from govoplan-core
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
# TypeScript cache
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
# Local WebUI test/build scratch directories
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
*$py.class
|
||||
# C extensions
|
||||
*.so
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
cover/
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# mypy
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
# Ruff stuff:
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
# ---> VisualStudioCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
*.db
|
||||
# GovOPlaN local runtime state
|
||||
runtime/
|
||||
# GovOPlaN WebUI test output
|
||||
@@ -0,0 +1,16 @@
|
||||
# GovOPlaN Admin Codex Guide
|
||||
|
||||
## Scope
|
||||
|
||||
This repository owns generic administration sections, governance templates, configuration packages, and operator-facing module lifecycle controls.
|
||||
|
||||
## Documentation Contract
|
||||
|
||||
- Treat documentation as part of every behavior change. Update this module's manifest-driven `DocumentationTopic` contributions for affected user and administrator behavior.
|
||||
- Keep feature content here; `govoplan-docs` projects it without importing Admin internals.
|
||||
- Maintain a static user/admin baseline and run `/mnt/DATA/git/govoplan/tools/checks/check-manifest-shapes.py` after behavior or manifest changes.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Consume module metadata and extension points; do not import optional module internals.
|
||||
- Keep package mutation in the trusted installer process rather than request handlers.
|
||||
@@ -1,11 +1,20 @@
|
||||
# GovOPlaN Admin
|
||||
|
||||
The Admin-owned workspace sections, lifecycle stages, consequence classes,
|
||||
contextual-help contract, and verification evidence are recorded in
|
||||
[docs/INTERFACE_PATTERN_MIGRATION.md](docs/INTERFACE_PATTERN_MIGRATION.md).
|
||||
|
||||
<!-- govoplan-repository-type:start -->
|
||||
**Repository type:** module (platform).
|
||||
<!-- govoplan-repository-type:end -->
|
||||
|
||||
`govoplan-admin` owns generic system administration API and WebUI contributions
|
||||
during the GovOPlaN module split.
|
||||
|
||||
This repository owns the live `governance_templates` and
|
||||
`governance_template_assignments` tables while preserving their historical
|
||||
table names. It contributes the stable
|
||||
This repository owns the live `admin_governance_templates` and
|
||||
`admin_governance_template_assignments` tables. Core migrations rename the
|
||||
historical unprefixed governance tables for existing development databases. 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.
|
||||
|
||||
@@ -23,3 +32,66 @@ section entries through core's `admin.sections` UI capability:
|
||||
|
||||
The route shell in access collects those sections at runtime, applies their
|
||||
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`.
|
||||
|
||||
The WebUI presents the lifecycle as five derived stages: plan, preflight,
|
||||
installer request, daemon execution, and run evidence. The projection resets
|
||||
when the saved plan changes and associates evidence only with an installer
|
||||
request created at or after the current plan revision. It therefore cannot make
|
||||
an old successful run look like evidence for a new plan. The earliest queue
|
||||
blocker is shown through Core's actionable blocker pattern with the required
|
||||
action, responsible operator or administrator, and destination. Contextual help
|
||||
uses the stable `admin.module-lifecycle-workflow` documentation topic.
|
||||
|
||||
## Tenant Module Entitlements
|
||||
|
||||
Deployment lifecycle and tenant availability are separate administration
|
||||
workflows. **System > Tenant modules** lets a system administrator select a
|
||||
tenant, mark installed modules unavailable, available, or forced, and set the
|
||||
tenant's current selection. **Tenant > Modules** lets an account with the
|
||||
`admin:module:write` permission change only the available selection. The
|
||||
`module_admin` role template grants the narrow read/write pair for that task.
|
||||
|
||||
Core closes required dependencies, retains protected administration modules,
|
||||
uses an optimistic entitlement revision, and records audit and governed
|
||||
configuration evidence. A selected module that is not globally active remains
|
||||
configured but unavailable at runtime. Module selection never grants module
|
||||
permissions.
|
||||
|
||||
User and group module visibility is configured through Views, where each WebUI
|
||||
module is represented by its root module surface. This keeps tenant operational
|
||||
state distinct from presentation preferences.
|
||||
|
||||
## Package Surfaces
|
||||
|
||||
The admin UI intentionally exposes two different package concepts:
|
||||
|
||||
- **Configuration packages** are import/export bundles for module-owned
|
||||
configuration data. They support dry-run diagnostics, approval, apply, and
|
||||
export workflows without installing Python or WebUI packages.
|
||||
- **Module package catalog** and **operator install plan** live under
|
||||
**Modules**. They describe approved release artifacts, install/update/remove
|
||||
plans, preflight blockers, maintenance-mode requirements, installer-daemon
|
||||
requests, and rollback visibility.
|
||||
|
||||
These surfaces should stay separate in navigation and copy. If future UI work
|
||||
combines them visually, it must still preserve the operator distinction between
|
||||
configuration mutation and package installation.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Admin interface pattern migration
|
||||
|
||||
This document records the interface-pattern coverage for the surfaces
|
||||
contributed by `govoplan-admin`. The Access module hosts the administration
|
||||
tree; Admin supplies only its declared sections through `admin.sections`.
|
||||
|
||||
## Surface inventory
|
||||
|
||||
| Surface | Archetype | Authority and state model |
|
||||
| --- | --- | --- |
|
||||
| Administration overview | Navigation dashboard | Counts are server projections. Links exist only for sections already admitted by capabilities, permissions, and the active View. |
|
||||
| System settings | Adaptive configuration | A local draft is compared with the saved server state. Reload preserves an unsaved draft, Save requires system write authority, and maintenance controls also require maintenance authority. |
|
||||
| Configuration changes | Governed work queue and evidence list | Open approval requests and immutable applied history are separate server-authoritative grids. Approval requires an explicit confirmation. |
|
||||
| Configuration packages | Guided preflight/apply/export workspace | JSON input is validated locally, dry-run evidence is shown separately, approval is optional where policy allows it, and application is explicitly confirmed. Reference selectors replace raw IDs unless historical manual mode is selected deliberately. |
|
||||
| Role and group templates | Governed definition directory | Definitions, tenant availability, and role permissions are edited in one draft. Deletion is confirmed and remains blocked by materialized dependencies in the backend. |
|
||||
| Module management | Guided lifecycle workflow and operations evidence | Desired runtime state, package plan, preflight, maintenance gate, daemon request, and durable run evidence are distinct stages. Disabled controls name the earliest blocker. |
|
||||
|
||||
## Consequence classes
|
||||
|
||||
- Overview navigation, reload, filtering, inspecting evidence, dry runs, and
|
||||
exports are reversible.
|
||||
- Settings, templates, desired module state, package plans, and approval
|
||||
requests are governed mutations with permission and validation reasons.
|
||||
- Approving a change, applying a configuration package, enabling maintenance,
|
||||
clearing a saved module plan, cancelling an installer request, and deleting
|
||||
a governance template require shared confirmation surfaces.
|
||||
- Actual package mutation remains outside the API process and is performed by
|
||||
the supervised installer. Run and rollback evidence remains durable.
|
||||
|
||||
## Shared controls and verification
|
||||
|
||||
Admin uses Core `AdminPageLayout`, `DataGrid`, `ReferenceSelect`, `StageRail`,
|
||||
`Dialog`, `ConfirmDialog`, `TableActionGroup`, `ActionBlockerHint`,
|
||||
`DocumentationHelpLink`, `ToggleSwitch`, and status/alert primitives. This
|
||||
inherits Core focus restoration, keyboard order, responsive overflow,
|
||||
disabled-action tooltips, and accessible dialog semantics.
|
||||
|
||||
The focused WebUI check rejects browser-native confirmation calls, private
|
||||
sibling-module imports, untranslated Admin-owned structural headings, missing
|
||||
contextual help, and missing disabled reasons on consequential controls. The
|
||||
manifest regression test fixes the help-context and surface declaration.
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/admin-webui",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "webui/src/index.ts",
|
||||
@@ -18,11 +18,11 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.5",
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.1"
|
||||
"@govoplan/core-webui": "^0.1.15",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-admin"
|
||||
version = "0.1.5"
|
||||
version = "0.1.15"
|
||||
description = "GovOPlaN generic administration module."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = [
|
||||
"govoplan-core>=0.1.5",
|
||||
"govoplan-access>=0.1.5",
|
||||
"govoplan-core>=0.1.15",
|
||||
"govoplan-access>=0.1.15",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -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.
File diff suppressed because it is too large
Load Diff
@@ -3,59 +3,10 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
RETENTION_DAY_KEYS = (
|
||||
"raw_campaign_json_retention_days",
|
||||
"generated_eml_retention_days",
|
||||
"stored_report_detail_retention_days",
|
||||
"mock_mailbox_retention_days",
|
||||
"audit_detail_retention_days",
|
||||
)
|
||||
|
||||
|
||||
RETENTION_POLICY_FIELD_KEYS = (
|
||||
"store_raw_campaign_json",
|
||||
*RETENTION_DAY_KEYS,
|
||||
"audit_detail_level",
|
||||
)
|
||||
|
||||
|
||||
def default_allow_lower_level_limits() -> dict[str, bool]:
|
||||
return {key: True for key in RETENTION_POLICY_FIELD_KEYS}
|
||||
|
||||
|
||||
def normalize_allow_lower_level_limits(value: Any, *, fill_defaults: bool) -> dict[str, bool] | None:
|
||||
if value in (None, ""):
|
||||
return default_allow_lower_level_limits() if fill_defaults else None
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("allow_lower_level_limits must be an object")
|
||||
normalized = default_allow_lower_level_limits() if fill_defaults else {}
|
||||
for key, allowed in value.items():
|
||||
clean_key = str(key)
|
||||
if clean_key not in RETENTION_POLICY_FIELD_KEYS:
|
||||
raise ValueError(f"Unknown retention policy field: {clean_key}")
|
||||
normalized[clean_key] = bool(allowed)
|
||||
return normalized
|
||||
|
||||
|
||||
class PrivacyRetentionPolicyItem(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
store_raw_campaign_json: bool = True
|
||||
raw_campaign_json_retention_days: int | None = Field(default=None, ge=0)
|
||||
generated_eml_retention_days: int | None = Field(default=None, ge=0)
|
||||
stored_report_detail_retention_days: int | None = Field(default=None, ge=0)
|
||||
mock_mailbox_retention_days: int | None = Field(default=None, ge=0)
|
||||
audit_detail_retention_days: int | None = Field(default=None, ge=0)
|
||||
audit_detail_level: Literal["full", "redacted", "minimal"] = "full"
|
||||
allow_lower_level_limits: dict[str, bool] = Field(default_factory=default_allow_lower_level_limits)
|
||||
|
||||
@field_validator("allow_lower_level_limits", mode="before")
|
||||
@classmethod
|
||||
def _normalize_allow_lower_level_limits(cls, value: Any) -> Any:
|
||||
return normalize_allow_lower_level_limits(value, fill_defaults=True)
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
from govoplan_core.privacy.schemas import PrivacyRetentionPolicyItem
|
||||
|
||||
|
||||
class MaintenanceModeItem(BaseModel):
|
||||
@@ -65,6 +16,14 @@ class MaintenanceModeItem(BaseModel):
|
||||
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):
|
||||
active_tenant_id: str
|
||||
active_tenant_name: str
|
||||
@@ -87,9 +46,21 @@ class SystemSettingsItem(BaseModel):
|
||||
allow_tenant_api_keys: bool = True
|
||||
privacy_retention_policy: PrivacyRetentionPolicyItem = Field(default_factory=PrivacyRetentionPolicyItem)
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@@ -99,6 +70,9 @@ class SystemSettingsUpdateRequest(BaseModel):
|
||||
allow_tenant_api_keys: bool
|
||||
privacy_retention_policy: PrivacyRetentionPolicyItem | 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):
|
||||
@@ -143,17 +117,76 @@ class ModuleStateUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled_modules: list[str] = Field(default_factory=list)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class TenantModuleEntitlementItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
dependencies: list[str] = Field(default_factory=list)
|
||||
runtime_active: bool
|
||||
availability: Literal["unavailable", "available", "forced"]
|
||||
selected: bool
|
||||
effective: bool
|
||||
forced: bool
|
||||
derived_dependency: bool
|
||||
tenant_can_toggle: bool
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class TenantModuleEntitlementResponse(BaseModel):
|
||||
tenant_id: str
|
||||
revision: int
|
||||
configured: bool
|
||||
available_modules: list[str] = Field(default_factory=list)
|
||||
forced_modules: list[str] = Field(default_factory=list)
|
||||
selected_modules: list[str] = Field(default_factory=list)
|
||||
effective_modules: list[str] = Field(default_factory=list)
|
||||
derived_dependencies: list[str] = Field(default_factory=list)
|
||||
modules: list[TenantModuleEntitlementItem] = Field(default_factory=list)
|
||||
diagnostics: list[dict[str, str]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TenantModuleTargetItem(BaseModel):
|
||||
id: str
|
||||
slug: str
|
||||
name: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
class TenantModuleTargetListResponse(BaseModel):
|
||||
tenants: list[TenantModuleTargetItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SystemTenantModulePolicyUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
available_modules: list[str] = Field(default_factory=list, max_length=1000)
|
||||
forced_modules: list[str] = Field(default_factory=list, max_length=1000)
|
||||
enabled_modules: list[str] = Field(default_factory=list, max_length=1000)
|
||||
expected_revision: int | None = Field(default=None, ge=0)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class TenantModuleSelectionUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled_modules: list[str] = Field(default_factory=list, max_length=1000)
|
||||
expected_revision: int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class ModuleInstallPlanItem(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
module_id: str = Field(min_length=1, max_length=120)
|
||||
action: Literal["install", "uninstall"]
|
||||
action: Literal["install", "update", "uninstall"]
|
||||
source: Literal["manual", "catalog"] = "manual"
|
||||
catalog: dict[str, Any] | None = None
|
||||
python_package: str | None = Field(default=None, max_length=200)
|
||||
python_ref: str | None = Field(default=None, max_length=1000)
|
||||
webui_package: str | None = Field(default=None, max_length=200)
|
||||
webui_ref: str | None = Field(default=None, max_length=1000)
|
||||
data_safety_acknowledged: bool = False
|
||||
destroy_data: bool = False
|
||||
status: Literal["planned", "applied", "blocked"] = "planned"
|
||||
notes: str | None = Field(default=None, max_length=1000)
|
||||
@@ -173,6 +206,63 @@ class ModuleInstallChecklistItem(BaseModel):
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
class ModuleInstallTargetItem(BaseModel):
|
||||
module_id: str
|
||||
action: Literal["install", "update", "uninstall"]
|
||||
source: Literal["manual", "catalog"]
|
||||
current_version: str | None = None
|
||||
target_version: str | None = None
|
||||
python_package: str | None = None
|
||||
python_ref: str | None = None
|
||||
webui_package: str | None = None
|
||||
webui_ref: str | None = None
|
||||
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
migration_notes: str | None = None
|
||||
current_version_min: str | None = None
|
||||
current_version_max_exclusive: str | None = None
|
||||
bridge_release: bool = False
|
||||
bridge_notes: str | None = None
|
||||
allow_downgrade: bool = False
|
||||
allow_same_version: bool = False
|
||||
recovery_tested: bool = False
|
||||
recovery_notes: str | None = None
|
||||
data_safety_acknowledged: bool = False
|
||||
|
||||
|
||||
class ModuleMigrationPlanStep(BaseModel):
|
||||
module_id: str
|
||||
action: Literal["install", "update", "uninstall"]
|
||||
phase: Literal["upgrade", "retirement"]
|
||||
source: Literal["manifest", "catalog", "pending"]
|
||||
has_migration_metadata: bool = False
|
||||
metadata_pending: bool = False
|
||||
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
current_version: str | None = None
|
||||
target_version: str | None = None
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class ModuleMigrationTaskPlanItem(BaseModel):
|
||||
module_id: str
|
||||
task_id: str
|
||||
phase: Literal["pre_migration_check", "pre_migration_prepare", "post_migration_backfill", "post_migration_verify"]
|
||||
summary: str
|
||||
task_version: str = "1"
|
||||
safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
idempotent: bool = True
|
||||
timeout_seconds: int | None = None
|
||||
source: Literal["manifest", "catalog", "pending"] = "manifest"
|
||||
has_executor: bool = False
|
||||
metadata_pending: bool = False
|
||||
|
||||
|
||||
class ModuleMigrationExecutionPlan(BaseModel):
|
||||
enabled_modules: list[str] = Field(default_factory=list)
|
||||
requires_database_migration: bool = False
|
||||
steps: list[ModuleMigrationPlanStep] = Field(default_factory=list)
|
||||
tasks: list[ModuleMigrationTaskPlanItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ModuleInstallPreflightResponse(BaseModel):
|
||||
allowed: bool
|
||||
maintenance_mode: bool
|
||||
@@ -182,6 +272,8 @@ class ModuleInstallPreflightResponse(BaseModel):
|
||||
rollback_commands: list[str] = Field(default_factory=list)
|
||||
issues: list[ModuleInstallPreflightIssue] = Field(default_factory=list)
|
||||
checklist: list[ModuleInstallChecklistItem] = Field(default_factory=list)
|
||||
target_plan: list[ModuleInstallTargetItem] = Field(default_factory=list)
|
||||
migration_plan: ModuleMigrationExecutionPlan = Field(default_factory=ModuleMigrationExecutionPlan)
|
||||
|
||||
|
||||
class ModuleInstallPlanResponse(BaseModel):
|
||||
@@ -213,6 +305,9 @@ class ModuleInstallerRunSummary(BaseModel):
|
||||
status: str
|
||||
started_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
|
||||
supervisor_status: str | None = None
|
||||
error: str | None = None
|
||||
@@ -224,6 +319,9 @@ class ModuleInstallerRunSummary(BaseModel):
|
||||
class ModuleInstallerRunListResponse(BaseModel):
|
||||
runs: list[ModuleInstallerRunSummary] = Field(default_factory=list)
|
||||
lock: ModuleInstallerLockStatus
|
||||
cursor: str | None = None
|
||||
next_cursor: str | None = None
|
||||
full: bool = False
|
||||
|
||||
|
||||
class ModuleInstallerRequestOptions(BaseModel):
|
||||
@@ -256,6 +354,7 @@ class ModuleInstallerRequestItem(BaseModel):
|
||||
cancelled_at: str | None = None
|
||||
cancelled_by: str | None = None
|
||||
retry_of: str | None = None
|
||||
trace: dict[str, Any] | None = None
|
||||
error: str | None = None
|
||||
record_path: str | None = None
|
||||
options: dict[str, Any] = Field(default_factory=dict)
|
||||
@@ -264,6 +363,9 @@ class ModuleInstallerRequestItem(BaseModel):
|
||||
class ModuleInstallerRequestListResponse(BaseModel):
|
||||
requests: list[ModuleInstallerRequestItem] = Field(default_factory=list)
|
||||
daemon: ModuleInstallerDaemonStatus
|
||||
cursor: str | None = None
|
||||
next_cursor: str | None = None
|
||||
full: bool = False
|
||||
|
||||
|
||||
class ModuleInstallerRequestCreateRequest(BaseModel):
|
||||
@@ -272,16 +374,44 @@ class ModuleInstallerRequestCreateRequest(BaseModel):
|
||||
options: ModuleInstallerRequestOptions = Field(default_factory=ModuleInstallerRequestOptions)
|
||||
|
||||
|
||||
class ModuleInterfaceProviderItem(BaseModel):
|
||||
name: str
|
||||
version: str
|
||||
|
||||
|
||||
class ModuleInterfaceRequirementItem(BaseModel):
|
||||
name: str
|
||||
version_min: str | None = None
|
||||
version_max_exclusive: str | None = None
|
||||
optional: bool = False
|
||||
|
||||
|
||||
class ModulePackageCatalogItem(BaseModel):
|
||||
module_id: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
version: str | None = None
|
||||
action: Literal["install", "uninstall"] = "install"
|
||||
action: Literal["install", "update", "uninstall"] = "install"
|
||||
dependencies: list[str] = Field(default_factory=list)
|
||||
optional_dependencies: list[str] = Field(default_factory=list)
|
||||
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
|
||||
migration_notes: str | None = None
|
||||
migration_after: list[str] = Field(default_factory=list)
|
||||
migration_before: list[str] = Field(default_factory=list)
|
||||
current_version_min: str | None = None
|
||||
current_version_max_exclusive: str | None = None
|
||||
bridge_release: bool = False
|
||||
bridge_notes: str | None = None
|
||||
allow_downgrade: bool = False
|
||||
allow_same_version: bool = False
|
||||
recovery_tested: bool = False
|
||||
recovery_notes: str | None = None
|
||||
python_package: str | None = None
|
||||
python_ref: str | None = None
|
||||
webui_package: str | None = None
|
||||
webui_ref: str | None = None
|
||||
provides_interfaces: list[ModuleInterfaceProviderItem] = Field(default_factory=list)
|
||||
requires_interfaces: list[ModuleInterfaceRequirementItem] = Field(default_factory=list)
|
||||
license_features: list[str] = Field(default_factory=list)
|
||||
license_allowed: bool = True
|
||||
license_enforced: bool = False
|
||||
@@ -291,6 +421,27 @@ class ModulePackageCatalogItem(BaseModel):
|
||||
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):
|
||||
modules: list[ModulePackageCatalogItem] = Field(default_factory=list)
|
||||
configured: bool = False
|
||||
@@ -298,13 +449,17 @@ class ModulePackageCatalogResponse(BaseModel):
|
||||
path: str | None = None
|
||||
source: str | None = None
|
||||
source_type: str | None = None
|
||||
cache_used: bool = False
|
||||
cache_path: str | None = None
|
||||
channel: str | None = None
|
||||
sequence: int | None = None
|
||||
generated_at: str | None = None
|
||||
not_before: str | None = None
|
||||
expires_at: str | None = None
|
||||
signed: bool = False
|
||||
trusted: bool = False
|
||||
key_id: str | None = None
|
||||
license: ModuleLicenseDiagnostics = Field(default_factory=ModuleLicenseDiagnostics)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
error: str | None = None
|
||||
|
||||
@@ -313,6 +468,7 @@ class ModuleInstallPlanUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
items: list[ModuleInstallPlanItem] = Field(default_factory=list)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class GovernanceAssignment(BaseModel):
|
||||
@@ -336,6 +492,22 @@ class GovernanceTemplateItem(BaseModel):
|
||||
|
||||
class GovernanceTemplateListResponse(BaseModel):
|
||||
templates: list[GovernanceTemplateItem]
|
||||
total: int = 0
|
||||
page: int = 1
|
||||
page_size: int = 500
|
||||
pages: int = 1
|
||||
|
||||
|
||||
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
|
||||
total: int = 0
|
||||
page: int = 1
|
||||
page_size: int = 500
|
||||
pages: int = 1
|
||||
|
||||
|
||||
class GovernanceTemplateCreateRequest(BaseModel):
|
||||
@@ -348,6 +520,7 @@ class GovernanceTemplateCreateRequest(BaseModel):
|
||||
permissions: list[str] = Field(default_factory=list)
|
||||
is_active: bool = True
|
||||
assignments: list[GovernanceAssignment] = Field(default_factory=list)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class GovernanceTemplateUpdateRequest(BaseModel):
|
||||
@@ -358,3 +531,4 @@ class GovernanceTemplateUpdateRequest(BaseModel):
|
||||
permissions: list[str] = Field(default_factory=list)
|
||||
is_active: bool = True
|
||||
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):
|
||||
__tablename__ = "governance_templates"
|
||||
__tablename__ = "admin_governance_templates"
|
||||
__table_args__ = (UniqueConstraint("kind", "slug", name="uq_governance_templates_kind_slug"),)
|
||||
|
||||
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):
|
||||
__tablename__ = "governance_template_assignments"
|
||||
__tablename__ = "admin_governance_template_assignments"
|
||||
__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)
|
||||
template_id: Mapped[str] = mapped_column(ForeignKey("governance_templates.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.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(String(36), nullable=False, index=True)
|
||||
mode: Mapped[str] = mapped_column(String(20), default="available", nullable=False)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from govoplan_core.core.access import (
|
||||
)
|
||||
from govoplan_core.core.runtime import get_registry
|
||||
from govoplan_core.security.permissions import validate_tenant_permissions
|
||||
from govoplan_tenancy.backend.db.models import Tenant
|
||||
from govoplan_core.tenancy.scope import Tenant
|
||||
|
||||
TEMPLATE_KINDS = {"group", "role"}
|
||||
ASSIGNMENT_MODES = {"available", "required"}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from govoplan_admin.backend.db import models as admin_models # noqa: F401 - populate Admin ORM metadata
|
||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||
from govoplan_core.core.modules import MigrationSpec, ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
MigrationSpec,
|
||||
ModuleContext,
|
||||
ModuleManifest,
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
@@ -13,12 +24,148 @@ def _route_factory(context: ModuleContext):
|
||||
return router
|
||||
|
||||
|
||||
ADMIN_PERMISSIONS = (
|
||||
PermissionDefinition(
|
||||
scope="admin:module:read",
|
||||
module_id="admin",
|
||||
resource="module",
|
||||
action="read",
|
||||
label="View tenant modules",
|
||||
description="Inspect module availability, requirements, and effective state for the active tenant.",
|
||||
category="Administration",
|
||||
level="tenant",
|
||||
),
|
||||
PermissionDefinition(
|
||||
scope="admin:module:write",
|
||||
module_id="admin",
|
||||
resource="module",
|
||||
action="write",
|
||||
label="Manage tenant modules",
|
||||
description="Enable or disable modules for the active tenant within system policy.",
|
||||
category="Administration",
|
||||
level="tenant",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ADMIN_ROLE_TEMPLATES = (
|
||||
RoleTemplate(
|
||||
slug="module_admin",
|
||||
name="Module administrator",
|
||||
description="Manage the active tenant's module selection within system policy.",
|
||||
permissions=("admin:module:read", "admin:module:write"),
|
||||
level="tenant",
|
||||
managed=True,
|
||||
protected=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
id="admin",
|
||||
name="Admin",
|
||||
version="0.1.5",
|
||||
dependencies=("access",),
|
||||
version="0.1.15",
|
||||
permissions=ADMIN_PERMISSIONS,
|
||||
role_templates=ADMIN_ROLE_TEMPLATES,
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
route_factory=_route_factory,
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="admin.workspace",
|
||||
title="Use the administration workspace",
|
||||
summary="The administration workspace shows only the sections supplied by enabled modules and allowed by the current account's permissions.",
|
||||
body="System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority.",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "system_admin", "operator"),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"help_contexts": [
|
||||
"admin.workspace",
|
||||
"admin.overview",
|
||||
"admin.section-navigation",
|
||||
],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="admin.tenant-module-entitlements",
|
||||
title="Govern modules per tenant",
|
||||
summary="System administrators set each tenant's module ceiling and forced modules; tenant module administrators choose within that ceiling.",
|
||||
body=(
|
||||
"Deployment activation installs and loads module code for the whole instance. Tenant module governance is a separate entitlement layer: system administrators mark modules unavailable, available, or forced for a tenant and may also change that tenant's selection. A tenant module administrator can only enable or disable available modules; forced modules and required dependencies remain effective. Module entitlement never grants permissions, and malformed policy fails closed to protected administration modules. Disabling a module stops new API, capability, schedule, and worker admission for that tenant; accepted durable work remains queued and requires an operator decision rather than being executed or discarded. Enabling a capability module such as Encryption only makes its services available; data encryption remains an explicit owning-module policy or migration decision."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
audience=("system_admin", "module_admin", "tenant_admin"),
|
||||
related_modules=("access", "policy", "views", "encryption"),
|
||||
metadata={
|
||||
"kind": "workflow",
|
||||
"help_contexts": [
|
||||
"admin.system-tenant-modules",
|
||||
"admin.tenant-modules",
|
||||
],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="admin.governance-and-module-lifecycle",
|
||||
title="Govern configuration and module lifecycle",
|
||||
summary="Admin owns reusable governance templates, configuration packages, and the operator-facing module lifecycle queue.",
|
||||
body="Configuration packages import or export module-owned configuration; they do not install software. Module catalog actions create reviewed install, update, activation, deactivation, or retirement requests for the trusted installer process. Governance templates materialize approved role and group structures through the owning Access contracts.",
|
||||
documentation_types=("admin",),
|
||||
audience=("system_admin", "operator", "module_admin"),
|
||||
related_modules=("access", "audit", "ops"),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"help_contexts": [
|
||||
"admin.system-settings",
|
||||
"admin.configuration-changes",
|
||||
"admin.configuration-packages",
|
||||
"admin.governance-templates",
|
||||
],
|
||||
},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="admin.module-lifecycle-workflow",
|
||||
title="Plan and supervise module lifecycle changes",
|
||||
summary="Move a reviewed module package plan through preflight, maintenance-gated queueing, daemon execution, and durable run evidence.",
|
||||
body=(
|
||||
"The Modules administration surface projects one operator workflow: save a package plan, resolve preflight findings, enter maintenance mode with the required authority, queue a supervised installer request, and inspect the matching run record. "
|
||||
"The stage indicator is derived from the saved plan timestamp, the latest matching request, and its run; an older request is never presented as evidence for a newer plan. "
|
||||
"Disabled queue actions name the earliest blocker, the person who can resolve it, and the plan surface where work continues. Package mutation remains outside the FastAPI process and recovery evidence remains durable in the installer ledger."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
audience=("system_admin", "operator", "module_admin"),
|
||||
related_modules=("ops", "audit"),
|
||||
metadata={
|
||||
"kind": "workflow",
|
||||
"context_ids": [
|
||||
"admin.module-lifecycle",
|
||||
"admin.module-lifecycle.queue-blocker",
|
||||
],
|
||||
"help_contexts": [
|
||||
"admin.module-lifecycle",
|
||||
"admin.module-lifecycle.queue-blocker",
|
||||
"admin.module-lifecycle.plan",
|
||||
"admin.module-lifecycle.evidence",
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
frontend=FrontendModule(
|
||||
module_id="admin",
|
||||
package_name="@govoplan/admin-webui",
|
||||
view_surfaces=(
|
||||
ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0),
|
||||
ViewSurface(id="admin.section.system-settings", module_id="admin", kind="section", label="System settings", order=10),
|
||||
ViewSurface(id="admin.section.system-configuration-changes", module_id="admin", kind="section", label="Configuration changes", order=20),
|
||||
ViewSurface(id="admin.section.system-configuration-packages", module_id="admin", kind="section", label="Configuration packages", order=30),
|
||||
ViewSurface(id="admin.section.system-role-templates", module_id="admin", kind="section", label="Role templates", order=40),
|
||||
ViewSurface(id="admin.section.system-groups", module_id="admin", kind="section", label="Group templates", order=50),
|
||||
ViewSurface(id="admin.section.system-modules", module_id="admin", kind="section", label="Modules", order=85),
|
||||
ViewSurface(id="admin.section.system-tenant-modules", module_id="admin", kind="section", label="Tenant modules", order=86),
|
||||
ViewSurface(id="admin.section.tenant-modules", module_id="admin", kind="section", label="Modules", order=60),
|
||||
),
|
||||
),
|
||||
migration_spec=MigrationSpec(module_id="admin", metadata=Base.metadata),
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
@@ -27,6 +174,17 @@ manifest = ModuleManifest(
|
||||
label="Admin",
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="runtime_meta",
|
||||
kind="presentation",
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="README.md",
|
||||
test_ref="tests/test_catalog_plan.py",
|
||||
known_limits=("Some module-specific administration surfaces still own their own navigation and release evidence.",),
|
||||
owned_concepts=("administration workspace", "configuration package workflow", "module lifecycle request"),
|
||||
non_owned_concepts=("module installation effect", "access policy", "module-owned settings"),
|
||||
operations_docs=("README.md",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""GovOPlaN Admin backend tests."""
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from govoplan_admin.backend.api.v1.routes import _catalog_plan_item
|
||||
|
||||
|
||||
class CatalogPlanItemTests(unittest.TestCase):
|
||||
def test_builds_catalog_item_and_preserves_catalog_metadata(self) -> None:
|
||||
validation: dict[str, object] = {
|
||||
"valid": True,
|
||||
"source": "https://catalog.example.test/modules.json",
|
||||
"source_type": "remote",
|
||||
"channel": "stable",
|
||||
"signed": True,
|
||||
"trusted": True,
|
||||
"modules": [
|
||||
"ignored",
|
||||
{"module_id": "other", "action": "install"},
|
||||
{
|
||||
"module_id": "calendar",
|
||||
"action": "install",
|
||||
"python_package": "govoplan-calendar",
|
||||
"python_ref": 42,
|
||||
"webui_package": "@govoplan/calendar-webui",
|
||||
"notes": "Catalog note",
|
||||
"license_features": ["calendar.sync", "", 7],
|
||||
},
|
||||
],
|
||||
}
|
||||
decision = {
|
||||
"allowed": True,
|
||||
"reason": "Feature expires soon.",
|
||||
"missing_features": ["calendar.future"],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"govoplan_admin.backend.api.v1.routes.module_license_decision",
|
||||
return_value=decision,
|
||||
) as license_decision:
|
||||
item, returned_validation = _catalog_plan_item(
|
||||
"calendar",
|
||||
{"calendar"},
|
||||
validation=validation,
|
||||
)
|
||||
|
||||
self.assertIs(returned_validation, validation)
|
||||
self.assertEqual(item.module_id, "calendar")
|
||||
self.assertEqual(item.action, "update")
|
||||
self.assertEqual(item.source, "catalog")
|
||||
self.assertEqual(item.python_package, "govoplan-calendar")
|
||||
self.assertIsNone(item.python_ref)
|
||||
self.assertEqual(item.webui_package, "@govoplan/calendar-webui")
|
||||
self.assertIsNone(item.webui_ref)
|
||||
self.assertEqual(item.notes, "Catalog note\nLicense warning: Feature expires soon.")
|
||||
self.assertEqual(item.catalog["source"], "https://catalog.example.test/modules.json")
|
||||
self.assertEqual(item.catalog["channel"], "stable")
|
||||
self.assertTrue(item.catalog["signed"])
|
||||
license_decision.assert_called_once_with(["calendar.sync", "7"])
|
||||
|
||||
def test_rejects_catalog_action_when_license_is_missing(self) -> None:
|
||||
validation: dict[str, object] = {
|
||||
"valid": True,
|
||||
"modules": [{"module_id": "calendar", "action": "install", "license_features": ["calendar.sync"]}],
|
||||
}
|
||||
with patch(
|
||||
"govoplan_admin.backend.api.v1.routes.module_license_decision",
|
||||
return_value={"allowed": False, "missing_features": ["calendar.sync", "calendar.write"]},
|
||||
), self.assertRaises(HTTPException) as raised:
|
||||
_catalog_plan_item("calendar", set(), validation=validation)
|
||||
|
||||
self.assertEqual(raised.exception.status_code, 403)
|
||||
self.assertEqual(
|
||||
raised.exception.detail,
|
||||
"License does not allow install for calendar: calendar.sync, calendar.write.",
|
||||
)
|
||||
|
||||
def test_rejects_invalid_or_missing_catalog_entries(self) -> None:
|
||||
with self.subTest("invalid catalog"), self.assertRaises(HTTPException) as invalid:
|
||||
_catalog_plan_item(
|
||||
"calendar",
|
||||
set(),
|
||||
validation={"valid": False, "error": "Signature is invalid."},
|
||||
)
|
||||
self.assertEqual(invalid.exception.status_code, 422)
|
||||
self.assertEqual(invalid.exception.detail, "Signature is invalid.")
|
||||
|
||||
with self.subTest("entry not found"), self.assertRaises(HTTPException) as missing:
|
||||
_catalog_plan_item(
|
||||
"calendar",
|
||||
set(),
|
||||
validation={
|
||||
"valid": True,
|
||||
"modules": [
|
||||
{"module_id": "calendar", "action": "remove"},
|
||||
{"module_id": "other", "action": "install"},
|
||||
],
|
||||
},
|
||||
)
|
||||
self.assertEqual(missing.exception.status_code, 404)
|
||||
self.assertEqual(missing.exception.detail, "Catalog install/update entry not found: calendar")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_admin.backend.manifest import manifest
|
||||
|
||||
|
||||
class InterfaceDocumentationContractTests(unittest.TestCase):
|
||||
def test_admin_topics_publish_stable_help_contexts(self) -> None:
|
||||
topics = {topic.id: topic for topic in manifest.documentation}
|
||||
expected = {
|
||||
"admin.workspace": {"admin.workspace", "admin.overview"},
|
||||
"admin.governance-and-module-lifecycle": {
|
||||
"admin.system-settings",
|
||||
"admin.configuration-changes",
|
||||
"admin.configuration-packages",
|
||||
"admin.governance-templates",
|
||||
},
|
||||
"admin.module-lifecycle-workflow": {
|
||||
"admin.module-lifecycle",
|
||||
"admin.module-lifecycle.queue-blocker",
|
||||
"admin.module-lifecycle.plan",
|
||||
"admin.module-lifecycle.evidence",
|
||||
},
|
||||
"admin.tenant-module-entitlements": {
|
||||
"admin.system-tenant-modules",
|
||||
"admin.tenant-modules",
|
||||
},
|
||||
}
|
||||
for topic_id, contexts in expected.items():
|
||||
self.assertIn(topic_id, topics)
|
||||
metadata = topics[topic_id].metadata or {}
|
||||
self.assertTrue(
|
||||
contexts.issubset(set(metadata.get("help_contexts", ()))),
|
||||
topic_id,
|
||||
)
|
||||
|
||||
def test_admin_contributed_surfaces_remain_declared(self) -> None:
|
||||
surface_ids = {
|
||||
surface.id for surface in manifest.frontend.view_surfaces
|
||||
}
|
||||
self.assertEqual(
|
||||
surface_ids,
|
||||
{
|
||||
"admin.section.overview",
|
||||
"admin.section.system-settings",
|
||||
"admin.section.system-configuration-changes",
|
||||
"admin.section.system-configuration-packages",
|
||||
"admin.section.system-role-templates",
|
||||
"admin.section.system-groups",
|
||||
"admin.section.system-modules",
|
||||
"admin.section.system-tenant-modules",
|
||||
"admin.section.tenant-modules",
|
||||
},
|
||||
)
|
||||
|
||||
def test_module_administrator_has_only_tenant_module_permissions(self) -> None:
|
||||
permissions = {item.scope for item in manifest.permissions}
|
||||
template = next(item for item in manifest.role_templates if item.slug == "module_admin")
|
||||
|
||||
self.assertEqual({"admin:module:read", "admin:module:write"}, permissions)
|
||||
self.assertEqual(tuple(sorted(permissions)), tuple(sorted(template.permissions)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+10
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/admin-webui",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -12,12 +12,16 @@
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test:installer-workflow": "node --experimental-strip-types --test tests/module-installer-workflow.test.ts",
|
||||
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.5",
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.1"
|
||||
"@govoplan/core-webui": "^0.1.15",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const root = resolve(import.meta.dirname, "..");
|
||||
const read = (path) => readFileSync(resolve(root, path), "utf8");
|
||||
|
||||
const overview = read("src/features/admin/AdminOverviewPanel.tsx");
|
||||
const settings = read("src/features/admin/SystemSettingsPanel.tsx");
|
||||
const changes = read("src/features/admin/ConfigurationChangesPanel.tsx");
|
||||
const packages = read("src/features/admin/ConfigurationPackagesPanel.tsx");
|
||||
const templates = read("src/features/admin/GovernanceTemplatesPanel.tsx");
|
||||
const modules = read("src/features/admin/ModuleManagementPanel.tsx");
|
||||
const moduleSource = read("src/module.ts");
|
||||
const allSource = [overview, settings, changes, packages, templates, modules].join("\n");
|
||||
|
||||
for (const source of [overview, settings, changes, packages, templates, modules]) {
|
||||
assert.match(source, /AdminPageLayout/);
|
||||
assert.match(source, /DocumentationHelpLink/);
|
||||
assert.match(source, /disabledReason|help=/);
|
||||
}
|
||||
|
||||
for (const source of [changes, packages, templates, modules]) {
|
||||
assert.match(source, /ConfirmDialog/);
|
||||
}
|
||||
|
||||
assert.match(changes, /DataGrid/);
|
||||
assert.match(packages, /ReferenceSelect/);
|
||||
assert.match(packages, /manualReferences/);
|
||||
assert.match(templates, /TableActionGroup/);
|
||||
assert.match(modules, /StageRail/);
|
||||
assert.match(modules, /ActionBlockerHint/);
|
||||
assert.match(moduleSource, /version: "0\.1\.8"/);
|
||||
|
||||
assert.doesNotMatch(overview, /title="(?:ADMINISTRATION|GLOBAL|TENANT|GROUP|USER)"/);
|
||||
assert.doesNotMatch(allSource, /window\.(alert|confirm|prompt)\s*\(/);
|
||||
assert.doesNotMatch(allSource, /@govoplan\/(?!core-webui)[^"']+-webui\//);
|
||||
|
||||
console.log("Admin interface pattern-language checks passed.");
|
||||
+437
-102
@@ -1,80 +1,28 @@
|
||||
import type { ApiSettings } from "@govoplan/core-webui";
|
||||
import { apiFetch } from "@govoplan/core-webui";
|
||||
|
||||
export type PermissionItem = {
|
||||
scope: string;
|
||||
label: string;
|
||||
description: string;
|
||||
category: string;
|
||||
level: "tenant" | "system";
|
||||
system_template_id?: string | null;
|
||||
system_required?: boolean;
|
||||
};
|
||||
|
||||
export type AdminOverview = {
|
||||
active_tenant_id: string;
|
||||
active_tenant_name: string;
|
||||
tenant_count?: number | null;
|
||||
system_account_count?: number | null;
|
||||
system_group_template_count?: number | null;
|
||||
system_role_template_count?: number | null;
|
||||
user_count: number;
|
||||
active_user_count: number;
|
||||
group_count: number;
|
||||
role_count: number;
|
||||
active_api_key_count: number;
|
||||
capabilities: string[];
|
||||
};
|
||||
|
||||
export type TenantAdminItem = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
default_locale: string;
|
||||
settings: Record<string, unknown>;
|
||||
allow_custom_groups?: boolean | null;
|
||||
allow_custom_roles?: boolean | null;
|
||||
allow_api_keys?: boolean | null;
|
||||
effective_governance: Record<string, boolean>;
|
||||
is_active: boolean;
|
||||
counts: Record<string, number>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type PrivacyRetentionPolicyFieldKey =
|
||||
| "store_raw_campaign_json"
|
||||
| "raw_campaign_json_retention_days"
|
||||
| "generated_eml_retention_days"
|
||||
| "stored_report_detail_retention_days"
|
||||
| "mock_mailbox_retention_days"
|
||||
| "audit_detail_retention_days"
|
||||
| "audit_detail_level";
|
||||
|
||||
export type PrivacyRetentionLimitPermissions = Record<PrivacyRetentionPolicyFieldKey, boolean>;
|
||||
export type PrivacyRetentionLimitPermissionPatch = Partial<PrivacyRetentionLimitPermissions>;
|
||||
|
||||
export type PrivacyRetentionPolicy = {
|
||||
store_raw_campaign_json: boolean;
|
||||
raw_campaign_json_retention_days?: number | null;
|
||||
generated_eml_retention_days?: number | null;
|
||||
stored_report_detail_retention_days?: number | null;
|
||||
mock_mailbox_retention_days?: number | null;
|
||||
audit_detail_retention_days?: number | null;
|
||||
audit_detail_level: "full" | "redacted" | "minimal";
|
||||
allow_lower_level_limits: PrivacyRetentionLimitPermissions;
|
||||
};
|
||||
|
||||
export type PrivacyRetentionPolicyPatch = Partial<Omit<PrivacyRetentionPolicy, "allow_lower_level_limits">> & {
|
||||
allow_lower_level_limits?: PrivacyRetentionLimitPermissionPatch;
|
||||
};
|
||||
import type { ApiSettings, DeltaDeletedItem, PrivacyRetentionPolicy } from "@govoplan/core-webui";
|
||||
import { apiFetch, apiPath, apiQuery } from "@govoplan/core-webui";
|
||||
export { fetchAdminOverview, fetchPermissionCatalog, fetchTenants } from "@govoplan/core-webui";
|
||||
export type {
|
||||
AdminOverview,
|
||||
PrivacyRetentionLimitPermissionPatch,
|
||||
PrivacyRetentionLimitPermissions,
|
||||
PrivacyRetentionPolicy,
|
||||
PrivacyRetentionPolicyFieldKey,
|
||||
PrivacyRetentionPolicyPatch,
|
||||
PermissionItem,
|
||||
TenantAdminItem
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
export type MaintenanceMode = {
|
||||
enabled: boolean;
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
export type LanguagePackage = {
|
||||
code: string;
|
||||
label: string;
|
||||
native_label?: string | null;
|
||||
};
|
||||
|
||||
export type SystemSettingsItem = {
|
||||
default_locale: string;
|
||||
allow_tenant_custom_groups: boolean;
|
||||
@@ -82,9 +30,20 @@ export type SystemSettingsItem = {
|
||||
allow_tenant_api_keys: boolean;
|
||||
privacy_retention_policy: PrivacyRetentionPolicy;
|
||||
maintenance_mode: MaintenanceMode;
|
||||
available_languages: LanguagePackage[];
|
||||
enabled_language_codes: string[];
|
||||
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 = {
|
||||
default_locale: string;
|
||||
allow_tenant_custom_groups: boolean;
|
||||
@@ -92,8 +51,103 @@ export type SystemSettingsUpdatePayload = {
|
||||
allow_tenant_api_keys: boolean;
|
||||
privacy_retention_policy?: PrivacyRetentionPolicy | 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 {
|
||||
return apiQuery(options);
|
||||
}
|
||||
|
||||
export type SystemSettingsDeltaResponse = {
|
||||
item?: SystemSettingsItem | null;
|
||||
sections: SystemSettingsDeltaSections;
|
||||
changed_sections: string[];
|
||||
} & DeltaResponseFields;
|
||||
|
||||
export type ConfigurationChangesDeltaResponse = {
|
||||
requests: ConfigurationChangeRequest[];
|
||||
history: ConfigurationChangeRecord[];
|
||||
} & DeltaResponseFields;
|
||||
|
||||
export type ModuleCatalogItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -132,13 +186,52 @@ export type ModuleCatalogResponse = {
|
||||
notes: string[];
|
||||
};
|
||||
|
||||
export type TenantModuleAvailability = "unavailable" | "available" | "forced";
|
||||
|
||||
export type TenantModuleEntitlementItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
dependencies: string[];
|
||||
runtime_active: boolean;
|
||||
availability: TenantModuleAvailability;
|
||||
selected: boolean;
|
||||
effective: boolean;
|
||||
forced: boolean;
|
||||
derived_dependency: boolean;
|
||||
tenant_can_toggle: boolean;
|
||||
reason?: string | null;
|
||||
};
|
||||
|
||||
export type TenantModuleEntitlementResponse = {
|
||||
tenant_id: string;
|
||||
revision: number;
|
||||
configured: boolean;
|
||||
available_modules: string[];
|
||||
forced_modules: string[];
|
||||
selected_modules: string[];
|
||||
effective_modules: string[];
|
||||
derived_dependencies: string[];
|
||||
modules: TenantModuleEntitlementItem[];
|
||||
diagnostics: Array<{ code: string; message: string; severity: string }>;
|
||||
};
|
||||
|
||||
export type TenantModuleTarget = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type ModuleInstallPlanItem = {
|
||||
module_id: string;
|
||||
action: "install" | "uninstall";
|
||||
action: "install" | "update" | "uninstall";
|
||||
source: "manual" | "catalog";
|
||||
catalog?: Record<string, unknown> | null;
|
||||
python_package?: string | null;
|
||||
python_ref?: string | null;
|
||||
webui_package?: string | null;
|
||||
webui_ref?: string | null;
|
||||
data_safety_acknowledged: boolean;
|
||||
destroy_data: boolean;
|
||||
status: "planned" | "applied" | "blocked";
|
||||
notes?: string | null;
|
||||
@@ -158,6 +251,63 @@ export type ModuleInstallChecklistItem = {
|
||||
detail?: string | null;
|
||||
};
|
||||
|
||||
export type ModuleInstallTargetItem = {
|
||||
module_id: string;
|
||||
action: "install" | "update" | "uninstall";
|
||||
source: "manual" | "catalog";
|
||||
current_version?: string | null;
|
||||
target_version?: string | null;
|
||||
python_package?: string | null;
|
||||
python_ref?: string | null;
|
||||
webui_package?: string | null;
|
||||
webui_ref?: string | null;
|
||||
migration_safety: "automatic" | "requires_review" | "forward_only" | "destructive";
|
||||
migration_notes?: string | null;
|
||||
current_version_min?: string | null;
|
||||
current_version_max_exclusive?: string | null;
|
||||
bridge_release: boolean;
|
||||
bridge_notes?: string | null;
|
||||
allow_downgrade: boolean;
|
||||
allow_same_version: boolean;
|
||||
recovery_tested: boolean;
|
||||
recovery_notes?: string | null;
|
||||
data_safety_acknowledged: boolean;
|
||||
};
|
||||
|
||||
export type ModuleMigrationPlanStep = {
|
||||
module_id: string;
|
||||
action: "install" | "update" | "uninstall";
|
||||
phase: "upgrade" | "retirement";
|
||||
source: "manifest" | "catalog" | "pending";
|
||||
has_migration_metadata: boolean;
|
||||
metadata_pending: boolean;
|
||||
migration_safety: "automatic" | "requires_review" | "forward_only" | "destructive";
|
||||
current_version?: string | null;
|
||||
target_version?: string | null;
|
||||
reason?: string | null;
|
||||
};
|
||||
|
||||
export type ModuleMigrationTaskPlanItem = {
|
||||
module_id: string;
|
||||
task_id: string;
|
||||
phase: "pre_migration_check" | "pre_migration_prepare" | "post_migration_backfill" | "post_migration_verify";
|
||||
summary: string;
|
||||
task_version: string;
|
||||
safety: "automatic" | "requires_review" | "forward_only" | "destructive";
|
||||
idempotent: boolean;
|
||||
timeout_seconds?: number | null;
|
||||
source: "manifest" | "catalog" | "pending";
|
||||
has_executor: boolean;
|
||||
metadata_pending: boolean;
|
||||
};
|
||||
|
||||
export type ModuleMigrationExecutionPlan = {
|
||||
enabled_modules: string[];
|
||||
requires_database_migration: boolean;
|
||||
steps: ModuleMigrationPlanStep[];
|
||||
tasks: ModuleMigrationTaskPlanItem[];
|
||||
};
|
||||
|
||||
export type ModuleInstallPreflight = {
|
||||
allowed: boolean;
|
||||
maintenance_mode: boolean;
|
||||
@@ -167,6 +317,8 @@ export type ModuleInstallPreflight = {
|
||||
rollback_commands: string[];
|
||||
issues: ModuleInstallPreflightIssue[];
|
||||
checklist: ModuleInstallChecklistItem[];
|
||||
target_plan: ModuleInstallTargetItem[];
|
||||
migration_plan: ModuleMigrationExecutionPlan;
|
||||
};
|
||||
|
||||
export type ModuleInstallPlanResponse = {
|
||||
@@ -196,6 +348,9 @@ export type ModuleInstallerRunSummary = {
|
||||
status: string;
|
||||
started_at?: string | null;
|
||||
finished_at?: string | null;
|
||||
request_id?: string | null;
|
||||
requested_by?: string | null;
|
||||
trace?: Record<string, unknown> | null;
|
||||
rollback_status?: string | null;
|
||||
supervisor_status?: string | null;
|
||||
error?: string | null;
|
||||
@@ -207,6 +362,9 @@ export type ModuleInstallerRunSummary = {
|
||||
export type ModuleInstallerRunListResponse = {
|
||||
runs: ModuleInstallerRunSummary[];
|
||||
lock: ModuleInstallerLockStatus;
|
||||
cursor?: string | null;
|
||||
next_cursor?: string | null;
|
||||
full?: boolean;
|
||||
};
|
||||
|
||||
export type ModuleInstallerRequestOptions = {
|
||||
@@ -235,6 +393,7 @@ export type ModuleInstallerRequestItem = {
|
||||
cancelled_at?: string | null;
|
||||
cancelled_by?: string | null;
|
||||
retry_of?: string | null;
|
||||
trace?: Record<string, unknown> | null;
|
||||
error?: string | null;
|
||||
record_path?: string | null;
|
||||
options: Record<string, unknown>;
|
||||
@@ -243,6 +402,21 @@ export type ModuleInstallerRequestItem = {
|
||||
export type ModuleInstallerRequestListResponse = {
|
||||
requests: ModuleInstallerRequestItem[];
|
||||
daemon: ModuleInstallerDaemonStatus;
|
||||
cursor?: string | null;
|
||||
next_cursor?: string | null;
|
||||
full?: boolean;
|
||||
};
|
||||
|
||||
export type ModuleInterfaceProviderItem = {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type ModuleInterfaceRequirementItem = {
|
||||
name: string;
|
||||
version_min?: string | null;
|
||||
version_max_exclusive?: string | null;
|
||||
optional: boolean;
|
||||
};
|
||||
|
||||
export type ModulePackageCatalogItem = {
|
||||
@@ -250,11 +424,27 @@ export type ModulePackageCatalogItem = {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
version?: string | null;
|
||||
action: "install" | "uninstall";
|
||||
action: "install" | "update" | "uninstall";
|
||||
dependencies: string[];
|
||||
optional_dependencies: string[];
|
||||
migration_safety: "automatic" | "requires_review" | "forward_only" | "destructive";
|
||||
migration_notes?: string | null;
|
||||
migration_after: string[];
|
||||
migration_before: string[];
|
||||
current_version_min?: string | null;
|
||||
current_version_max_exclusive?: string | null;
|
||||
bridge_release: boolean;
|
||||
bridge_notes?: string | null;
|
||||
allow_downgrade: boolean;
|
||||
allow_same_version: boolean;
|
||||
recovery_tested: boolean;
|
||||
recovery_notes?: string | null;
|
||||
python_package?: string | null;
|
||||
python_ref?: string | null;
|
||||
webui_package?: string | null;
|
||||
webui_ref?: string | null;
|
||||
provides_interfaces: ModuleInterfaceProviderItem[];
|
||||
requires_interfaces: ModuleInterfaceRequirementItem[];
|
||||
license_features: string[];
|
||||
license_allowed: boolean;
|
||||
license_enforced: boolean;
|
||||
@@ -264,6 +454,27 @@ export type ModulePackageCatalogItem = {
|
||||
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 = {
|
||||
modules: ModulePackageCatalogItem[];
|
||||
configured: boolean;
|
||||
@@ -271,13 +482,17 @@ export type ModulePackageCatalogResponse = {
|
||||
path?: string | null;
|
||||
source?: string | null;
|
||||
source_type?: string | null;
|
||||
cache_used: boolean;
|
||||
cache_path?: string | null;
|
||||
channel?: string | null;
|
||||
sequence?: number | null;
|
||||
generated_at?: string | null;
|
||||
not_before?: string | null;
|
||||
expires_at?: string | null;
|
||||
signed: boolean;
|
||||
trusted: boolean;
|
||||
key_id?: string | null;
|
||||
license: ModuleLicenseDiagnostics;
|
||||
warnings: string[];
|
||||
error?: string | null;
|
||||
};
|
||||
@@ -301,24 +516,15 @@ export type GovernanceTemplateItem = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export function fetchAdminOverview(settings: ApiSettings): Promise<AdminOverview> {
|
||||
return apiFetch(settings, "/api/v1/admin/overview");
|
||||
}
|
||||
|
||||
export async function fetchPermissionCatalog(settings: ApiSettings): Promise<PermissionItem[]> {
|
||||
const response = await apiFetch<{ permissions: PermissionItem[] }>(settings, "/api/v1/admin/permissions");
|
||||
return response.permissions;
|
||||
}
|
||||
|
||||
export async function fetchTenants(settings: ApiSettings): Promise<TenantAdminItem[]> {
|
||||
const response = await apiFetch<{ tenants: TenantAdminItem[] }>(settings, "/api/v1/admin/tenants");
|
||||
return response.tenants;
|
||||
}
|
||||
|
||||
export function fetchSystemSettings(settings: ApiSettings): Promise<SystemSettingsItem> {
|
||||
return apiFetch(settings, "/api/v1/admin/system/settings");
|
||||
}
|
||||
|
||||
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> {
|
||||
return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) });
|
||||
}
|
||||
@@ -327,10 +533,50 @@ export function fetchModuleCatalog(settings: ApiSettings): Promise<ModuleCatalog
|
||||
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", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ enabled_modules: enabledModules })
|
||||
body: JSON.stringify({ enabled_modules: enabledModules, change_request_id: changeRequestId ?? null })
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchSystemTenantModules(settings: ApiSettings, tenantId: string): Promise<TenantModuleEntitlementResponse> {
|
||||
return apiFetch(settings, `/api/v1/admin/system/tenants/${encodeURIComponent(tenantId)}/modules`);
|
||||
}
|
||||
|
||||
export async function fetchTenantModuleTargets(settings: ApiSettings): Promise<TenantModuleTarget[]> {
|
||||
const response = await apiFetch<{ tenants: TenantModuleTarget[] }>(settings, "/api/v1/admin/system/tenant-module-targets");
|
||||
return response.tenants;
|
||||
}
|
||||
|
||||
export function updateSystemTenantModules(
|
||||
settings: ApiSettings,
|
||||
tenantId: string,
|
||||
payload: {
|
||||
available_modules: string[];
|
||||
forced_modules: string[];
|
||||
enabled_modules: string[];
|
||||
expected_revision: number;
|
||||
change_request_id?: string | null;
|
||||
}
|
||||
): Promise<TenantModuleEntitlementResponse> {
|
||||
return apiFetch(settings, `/api/v1/admin/system/tenants/${encodeURIComponent(tenantId)}/modules`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchTenantModules(settings: ApiSettings): Promise<TenantModuleEntitlementResponse> {
|
||||
return apiFetch(settings, "/api/v1/admin/tenant/modules");
|
||||
}
|
||||
|
||||
export function updateTenantModules(
|
||||
settings: ApiSettings,
|
||||
payload: { enabled_modules: string[]; expected_revision: number }
|
||||
): Promise<TenantModuleEntitlementResponse> {
|
||||
return apiFetch(settings, "/api/v1/admin/tenant/modules", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -338,12 +584,12 @@ export function fetchModuleInstallPlan(settings: ApiSettings): Promise<ModuleIns
|
||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan");
|
||||
}
|
||||
|
||||
export function fetchModuleInstallerRuns(settings: ApiSettings): Promise<ModuleInstallerRunListResponse> {
|
||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-runs");
|
||||
export function fetchModuleInstallerRuns(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRunListResponse> {
|
||||
return apiFetch(settings, apiPath("/api/v1/admin/system/modules/install-runs", options));
|
||||
}
|
||||
|
||||
export function fetchModuleInstallerRequests(settings: ApiSettings): Promise<ModuleInstallerRequestListResponse> {
|
||||
return apiFetch(settings, "/api/v1/admin/system/modules/install-requests");
|
||||
export function fetchModuleInstallerRequests(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRequestListResponse> {
|
||||
return apiFetch(settings, apiPath("/api/v1/admin/system/modules/install-requests", options));
|
||||
}
|
||||
|
||||
export function createModuleInstallerRequest(settings: ApiSettings, options: ModuleInstallerRequestOptions): Promise<ModuleInstallerRequestItem> {
|
||||
@@ -373,10 +619,10 @@ export function planModuleUninstall(settings: ApiSettings, moduleId: string): Pr
|
||||
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", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ items })
|
||||
body: JSON.stringify({ items, change_request_id: changeRequestId ?? null })
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,19 +631,108 @@ export function clearModuleInstallPlan(settings: ApiSettings): Promise<ModuleIns
|
||||
}
|
||||
|
||||
export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "group" | "role"): Promise<GovernanceTemplateItem[]> {
|
||||
const suffix = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
||||
const response = await apiFetch<{ templates: GovernanceTemplateItem[] }>(settings, `/api/v1/admin/system/governance-templates${suffix}`);
|
||||
return response.templates;
|
||||
const pageSize = 500;
|
||||
const templates: GovernanceTemplateItem[] = [];
|
||||
for (let page = 1; ; page += 1) {
|
||||
const response = await apiFetch<{
|
||||
templates: GovernanceTemplateItem[];
|
||||
pages?: number;
|
||||
}>(
|
||||
settings,
|
||||
apiPath("/api/v1/admin/system/governance-templates", {
|
||||
kind,
|
||||
page,
|
||||
page_size: pageSize
|
||||
})
|
||||
);
|
||||
templates.push(...response.templates);
|
||||
if (page >= (response.pages ?? 1)) {
|
||||
return 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) });
|
||||
}
|
||||
|
||||
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) });
|
||||
}
|
||||
|
||||
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string): Promise<void> {
|
||||
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}`, { method: "DELETE" });
|
||||
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string, changeRequestId?: string | null): Promise<void> {
|
||||
return apiFetch(settings, apiPath(`/api/v1/admin/system/governance-templates/${templateId}`, { change_request_id: changeRequestId }), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> {
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ApiSettings } from "@govoplan/core-webui";
|
||||
import { fetchAdminOverview, type AdminOverview } from "../../api/admin";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { Card, MetricCard } from "@govoplan/core-webui";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui";
|
||||
import { AdminPageLayout, DocumentationHelpLink, adminErrorMessage } from "@govoplan/core-webui";
|
||||
import { ADMIN_INTERFACE_I18N, ADMIN_WORKSPACE_DOCUMENTATION } from "./interfacePatterns";
|
||||
|
||||
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 [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -13,81 +14,118 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try { setOverview(await fetchAdminOverview(settings)); }
|
||||
catch (err) { setError(adminErrorMessage(err)); }
|
||||
finally { setLoading(false); }
|
||||
try {setOverview(await fetchAdminOverview(settings));}
|
||||
catch (err) {setError(adminErrorMessage(err));} finally
|
||||
{setLoading(false);}
|
||||
}
|
||||
|
||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
|
||||
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl]);
|
||||
|
||||
const hasSystemMetrics = Boolean(overview && [
|
||||
overview.tenant_count,
|
||||
overview.system_account_count,
|
||||
overview.system_group_template_count,
|
||||
overview.system_role_template_count
|
||||
].some((value) => value !== null && value !== undefined));
|
||||
overview.tenant_count,
|
||||
overview.system_account_count,
|
||||
overview.system_group_template_count,
|
||||
overview.system_role_template_count].
|
||||
some((value) => value !== null && value !== undefined));
|
||||
|
||||
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={<><DocumentationHelpLink reference={ADMIN_WORKSPACE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : undefined}>i18n:govoplan-admin.reload.cce71553</Button></>}>
|
||||
{overview && <>
|
||||
{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">
|
||||
<Metric title="Tenants" value={overview.tenant_count ?? "—"} text="Registered tenant spaces." />
|
||||
<Metric title="Users" value={overview.system_account_count ?? "—"} text="Global login accounts across all tenants." />
|
||||
<Metric title="Central groups" value={overview.system_group_template_count ?? "—"} text="System-governed group definitions." />
|
||||
<Metric title="Tenant roles" value={overview.system_role_template_count ?? "—"} text="Centrally governed tenant roles." />
|
||||
<Metric title="i18n:govoplan-admin.tenants.1f7ae776" value={overview.tenant_count ?? "—"} text="i18n:govoplan-admin.registered_tenant_spaces.61e17d70" />
|
||||
<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="i18n:govoplan-admin.central_groups.5c9b5b66" value={overview.system_group_template_count ?? "—"} text="i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9" />
|
||||
<Metric title="i18n:govoplan-admin.tenant_roles.51aca82d" value={overview.system_role_template_count ?? "—"} text="i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b" />
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{hasSystemArea(availableSections) && <Card title="System administration">
|
||||
{hasAnySection(availableSections, platformSectionIds) && <Card title={ADMIN_INTERFACE_I18N.administrationHeading}>
|
||||
<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-tenants") && <AreaLink title="Tenants" text="Create, suspend and govern tenant spaces." onClick={() => onSelect("system-tenants")} />}
|
||||
{availableSections.has("system-roles") && <AreaLink title="System roles" text="Instance-wide roles assigned directly to global accounts." onClick={() => onSelect("system-roles")} />}
|
||||
{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-groups") && <AreaLink title="Groups" text="Group definitions made available or required in selected tenants." onClick={() => onSelect("system-groups")} />}
|
||||
{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")} />}
|
||||
{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-tenant-modules") && <AreaLink title="Tenant modules" text="Set per-tenant availability, forced modules, and current selection." onClick={() => onSelect("system-tenant-modules")} />}
|
||||
{availableSections.has("system-configuration-packages") && <AreaLink title="i18n:govoplan-admin.configuration_packages.eb2f05f1" text="i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8" onClick={() => onSelect("system-configuration-packages")} />}
|
||||
{availableSections.has("system-settings") && <AreaLink title="i18n:govoplan-admin.maintenance.94de303b" text="i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa" onClick={() => onSelect("system-settings")} />}
|
||||
{availableSections.has("system-configuration-changes") && <AreaLink title="i18n:govoplan-admin.changes.8aa57de6" text="i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335" onClick={() => onSelect("system-configuration-changes")} />}
|
||||
{availableSections.has("system-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />}
|
||||
</div>
|
||||
</Card>}
|
||||
|
||||
<div className="admin-overview-section-label">Active tenant: {overview.active_tenant_name}</div>
|
||||
{hasAnySection(availableSections, globalSectionIds) && <Card title={ADMIN_INTERFACE_I18N.globalHeading}>
|
||||
<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")} />}
|
||||
{availableSections.has("system-view-policy") && <AreaLink title="View policy" text="Limit View actions, definitions, and surfaces across the instance." onClick={() => onSelect("system-view-policy")} />}
|
||||
</div>
|
||||
</Card>}
|
||||
|
||||
<div className="admin-overview-section-label">i18n:govoplan-admin.active_tenant.dfadf0aa {overview.active_tenant_name}</div>
|
||||
<div className="metric-grid">
|
||||
<Metric title="Tenant users" value={`${overview.active_user_count}/${overview.user_count}`} text="Active and total memberships." />
|
||||
<Metric title="Groups" value={overview.group_count} text="Tenant-local and centrally managed groups." />
|
||||
<Metric title="Roles" value={overview.role_count} text="Tenant-local and centrally managed roles." />
|
||||
<Metric title="API keys" value={overview.active_api_key_count} text="Active tenant automation credentials." />
|
||||
<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="i18n:govoplan-admin.groups.ae9629f4" value={overview.group_count} text="i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5" />
|
||||
<Metric title="i18n:govoplan-admin.roles.47dcc27d" value={overview.role_count} text="i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2" />
|
||||
<Metric title="i18n:govoplan-admin.api_keys.94fcf3c2" value={overview.active_api_key_count} text="i18n:govoplan-admin.active_tenant_automation_credentials.240c659e" />
|
||||
</div>
|
||||
|
||||
<Card title="Tenant administration">
|
||||
{hasAnySection(availableSections, tenantSectionIds) && <Card title={ADMIN_INTERFACE_I18N.tenantHeading}>
|
||||
<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="Roles" text="Tenant permission bundles and system-managed role copies." onClick={() => onSelect("tenant-roles")} />}
|
||||
{availableSections.has("tenant-groups") && <AreaLink title="Groups" text="Tenant memberships and inherited roles." onClick={() => onSelect("tenant-groups")} />}
|
||||
{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-mail-servers") && <AreaLink title="Mail servers" text="Reusable encrypted SMTP/IMAP profiles and mail policy." 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="API keys" text="Scoped automation credentials capped by owner permissions." onClick={() => onSelect("tenant-api-keys")} />}
|
||||
{availableSections.has("tenant-audit") && <AreaLink title="Audit" text="Tenant-level administrative history only." onClick={() => onSelect("tenant-audit")} />}
|
||||
{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-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-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-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="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-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-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-modules") && <AreaLink title="Modules" text="Choose modules made available to this tenant by system policy." onClick={() => onSelect("tenant-modules")} />}
|
||||
{availableSections.has("tenant-view-policy") && <AreaLink title="View policy" text="Narrow inherited View actions and available surfaces for this tenant." onClick={() => onSelect("tenant-view-policy")} />}
|
||||
{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>
|
||||
</Card>
|
||||
</Card>}
|
||||
|
||||
{hasAnySection(availableSections, groupSectionIds) && <Card title={ADMIN_INTERFACE_I18N.groupHeading}>
|
||||
<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")} />}
|
||||
{availableSections.has("group-view-policy") && <AreaLink title="View policy" text="Set View limits for a selected group." onClick={() => onSelect("group-view-policy")} />}
|
||||
</div>
|
||||
</Card>}
|
||||
|
||||
{hasAnySection(availableSections, userSectionIds) && <Card title={ADMIN_INTERFACE_I18N.userHeading}>
|
||||
<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")} />}
|
||||
{availableSections.has("user-view-policy") && <AreaLink title="View policy" text="Set View limits for a selected user." onClick={() => onSelect("user-view-policy")} />}
|
||||
</div>
|
||||
</Card>}
|
||||
</>}
|
||||
</AdminPageLayout>
|
||||
);
|
||||
</AdminPageLayout>);
|
||||
|
||||
}
|
||||
|
||||
function hasSystemArea(sections: ReadonlySet<string>): boolean {
|
||||
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 platformSectionIds = ["system-modules", "system-tenant-modules", "system-configuration-packages", "system-settings", "system-configuration-changes", "system-audit"];
|
||||
const globalSectionIds = ["system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-file-connectors", "system-mail-servers", "system-retention", "system-view-policy"];
|
||||
const tenantSectionIds = ["tenant-roles", "tenant-groups", "tenant-users", "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", "tenant-retention", "tenant-view-policy", "tenant-modules", "tenant-settings", "tenant-audit"];
|
||||
const groupSectionIds = ["tenant-group-file-connectors", "tenant-group-mail-servers", "tenant-group-retention", "group-view-policy"];
|
||||
const userSectionIds = ["tenant-user-file-connectors", "tenant-user-mail-servers", "tenant-user-retention", "user-view-policy"];
|
||||
|
||||
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 }) {
|
||||
return <Card title={title}><strong className="module-big-number">{value}</strong><p className="muted">{text}</p></Card>;
|
||||
function Metric({ title, value, text }: {title: string;value: string | number;text: string;}) {
|
||||
return <MetricCard label={title} value={value} detail={text} />;
|
||||
}
|
||||
|
||||
function AreaLink({ title, text, onClick }: { title: string; text: string; onClick: () => void }) {
|
||||
function AreaLink({ title, text, onClick }: {title: string;text: string;onClick: () => void;}) {
|
||||
return <button className="admin-overview-link" onClick={onClick}><strong>{title}</strong><span>{text}</span></button>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Check, RefreshCw } from "lucide-react";
|
||||
import type { ApiSettings } from "@govoplan/core-webui";
|
||||
import { AdminPageLayout, Button, Card, ConfirmDialog, DataGrid, DocumentationHelpLink, StatusBadge, TableActionGroup, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks, type DataGridColumn } from "@govoplan/core-webui";
|
||||
import {
|
||||
approveConfigurationChangeRequest,
|
||||
fetchConfigurationChangesDelta,
|
||||
type ConfigurationChangeRecord,
|
||||
type ConfigurationChangeRequest } from
|
||||
"../../api/admin";
|
||||
import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N } from "./interfacePatterns";
|
||||
|
||||
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 [approving, setApproving] = useState<ConfigurationChangeRequest | null>(null);
|
||||
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("");
|
||||
setApproving(null);
|
||||
}
|
||||
}
|
||||
|
||||
const pending = useMemo(() => requests.filter((item) => item.status !== "applied" && item.status !== "rejected"), [requests]);
|
||||
const requestColumns: DataGridColumn<ConfigurationChangeRequest>[] = [
|
||||
{
|
||||
id: "setting",
|
||||
header: "i18n:govoplan-admin.setting.fb449f71",
|
||||
width: "minmax(220px, 1fr)",
|
||||
minWidth: 180,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
value: (request) => `${request.label || request.key} ${request.key}`,
|
||||
render: (request) => <div><strong>{request.label || request.key}</strong><span className="muted block">{request.key}</span></div>
|
||||
},
|
||||
{ id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 150, sortable: true, filterable: true, value: (request) => request.status, render: (request) => <StatusBadge status={statusTone(request.status)} label={request.status} /> },
|
||||
{ id: "requested", header: "i18n:govoplan-admin.requested.c26bf60f", width: 180, sortable: true, value: (request) => request.requested_at, render: (request) => formatDateTime(request.requested_at) },
|
||||
{ id: "approvals", header: "i18n:govoplan-admin.approvals.deb9d03c", width: 110, sortable: true, value: (request) => request.approvals.length },
|
||||
{ id: "target", header: "i18n:govoplan-admin.target.61ad50a9", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, filterable: true, value: (request) => targetLabel(request.target), render: (request) => targetLabel(request.target) },
|
||||
{
|
||||
id: "actions",
|
||||
header: "i18n:govoplan-admin.action.97c89a4d",
|
||||
width: 72,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (request) => <TableActionGroup actions={[{
|
||||
id: "approve",
|
||||
label: "i18n:govoplan-admin.approve.7b2c7f14",
|
||||
icon: <Check size={16} aria-hidden="true" />,
|
||||
applicable: request.status === "pending_approval",
|
||||
disabled: !canApprove || Boolean(busyId),
|
||||
disabledReason: request.status !== "pending_approval"
|
||||
? "i18n:govoplan-admin.request_is_not_pending_approval.6bf3c031"
|
||||
: !canApprove
|
||||
? ADMIN_INTERFACE_I18N.governanceWriteRequired
|
||||
: busyId
|
||||
? ADMIN_INTERFACE_I18N.busy
|
||||
: undefined,
|
||||
onClick: () => setApproving(request)
|
||||
}]} />
|
||||
}
|
||||
];
|
||||
|
||||
const historyColumns: DataGridColumn<ConfigurationChangeRecord>[] = [
|
||||
{ id: "version", header: "i18n:govoplan-admin.version.2da600bf", width: 100, sortable: true, value: (record) => record.version, render: (record) => `#${record.version}` },
|
||||
{ id: "setting", header: "i18n:govoplan-admin.setting.fb449f71", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (record) => `${record.key} ${record.id}`, render: (record) => <div><strong>{record.key}</strong><span className="muted block">{record.id}</span></div> },
|
||||
{ id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 150, sortable: true, filterable: true, value: (record) => record.status, render: (record) => <StatusBadge status={statusTone(record.status)} label={record.status} /> },
|
||||
{ id: "applied", header: "i18n:govoplan-admin.applied.a3e4a569", width: 180, sortable: true, value: (record) => record.created_at, render: (record) => formatDateTime(record.created_at) },
|
||||
{ id: "approvers", header: "i18n:govoplan-admin.approvers.0e2de1fb", width: 120, sortable: true, value: (record) => record.approval_user_ids.length, render: (record) => record.approval_user_ids.length || "-" },
|
||||
{ id: "target", header: "i18n:govoplan-admin.target.61ad50a9", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, filterable: true, value: (record) => targetLabel(record.target), render: (record) => targetLabel(record.target) }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || Boolean(busyId)} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busyId ? ADMIN_INTERFACE_I18N.busy : undefined}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button></>}>
|
||||
|
||||
<Card title="i18n:govoplan-admin.requests.f7194e6a">
|
||||
<DataGrid
|
||||
id="admin-configuration-change-requests"
|
||||
rows={pending}
|
||||
columns={requestColumns}
|
||||
getRowKey={(request) => request.id}
|
||||
emptyText="i18n:govoplan-admin.no_open_requests.580c95f9"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-admin.history.90ccd649">
|
||||
<DataGrid
|
||||
id="admin-configuration-change-history"
|
||||
rows={history}
|
||||
columns={historyColumns}
|
||||
getRowKey={(record) => record.id}
|
||||
emptyText="i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7"
|
||||
/>
|
||||
</Card>
|
||||
</AdminPageLayout>
|
||||
<ConfirmDialog
|
||||
open={Boolean(approving)}
|
||||
title={ADMIN_INTERFACE_I18N.approveTitle}
|
||||
message={ADMIN_INTERFACE_I18N.approveMessage}
|
||||
confirmLabel="i18n:govoplan-admin.approve.7b2c7f14"
|
||||
busy={Boolean(busyId)}
|
||||
onCancel={() => setApproving(null)}
|
||||
onConfirm={() => approving && void approve(approving)}
|
||||
/>
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Check, Download, Play, RefreshCw } from "lucide-react";
|
||||
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
||||
import { AdminPageLayout, Button, Card, ConfirmDialog, DataGrid, DocumentationHelpLink, FormField, ReferenceSelect, StatusBadge, ToggleSwitch, adminErrorMessage, i18nMessage, type DataGridColumn } from "@govoplan/core-webui";
|
||||
import {
|
||||
applyConfigurationPackage,
|
||||
createConfigurationChangeRequest,
|
||||
dryRunConfigurationPackage,
|
||||
exportConfigurationPackage,
|
||||
fetchConfigurationPackageCatalogValidation,
|
||||
type ConfigurationChangeRequest,
|
||||
type ConfigurationPackageDiagnostic,
|
||||
type ConfigurationPackagePlanItem,
|
||||
type ConfigurationPackageRequiredData } from
|
||||
"../../api/admin";
|
||||
import {
|
||||
createChangeRequestReferenceProvider,
|
||||
createTenantReferenceProvider
|
||||
} from "./configurationReferenceProviders";
|
||||
import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N } from "./interfacePatterns";
|
||||
|
||||
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, auth, canWrite }: {settings: ApiSettings;auth: AuthInfo;canWrite: boolean;}) {
|
||||
const [catalogValidation, setCatalogValidation] = useState<Record<string, unknown> | null>(null);
|
||||
const [packageText, setPackageText] = useState(() => JSON.stringify(SAMPLE_ACCESS_PACKAGE, null, 2));
|
||||
const activeTenantId = auth.active_tenant?.id ?? auth.tenant.id;
|
||||
const [tenantId, setTenantId] = useState(activeTenantId);
|
||||
const [suppliedDataText, setSuppliedDataText] = useState("{}");
|
||||
const [changeRequestId, setChangeRequestId] = useState("");
|
||||
const [manualReferences, setManualReferences] = useState(false);
|
||||
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 [confirmApply, setConfirmApply] = useState(false);
|
||||
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);
|
||||
const tenantProvider = useMemo(
|
||||
() => createTenantReferenceProvider(settings),
|
||||
[settings.accessToken, settings.apiBaseUrl, settings.apiKey]
|
||||
);
|
||||
const changeRequestProvider = useMemo(
|
||||
() => createChangeRequestReferenceProvider(settings, {
|
||||
purpose: "configuration_packages.apply",
|
||||
tenantId
|
||||
}),
|
||||
[
|
||||
settings.accessToken,
|
||||
settings.apiBaseUrl,
|
||||
settings.apiKey,
|
||||
tenantId
|
||||
]
|
||||
);
|
||||
|
||||
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]);
|
||||
useEffect(() => {
|
||||
setTenantId((current) => current || activeTenantId);
|
||||
}, [activeTenantId]);
|
||||
|
||||
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={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void loadCatalog()} disabled={loading || Boolean(busy)} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}><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">
|
||||
<div className="wide">
|
||||
<ToggleSwitch
|
||||
checked={manualReferences}
|
||||
onChange={setManualReferences}
|
||||
label={ADMIN_INTERFACE_I18N.enterReferencesManually}
|
||||
help={ADMIN_INTERFACE_I18N.manualReferenceHelp}
|
||||
/>
|
||||
</div>
|
||||
<div className="wide">
|
||||
<FormField label="i18n:govoplan-admin.tenant_id.59eba244" documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
|
||||
{manualReferences ? (
|
||||
<input value={tenantId} onChange={(event) => setTenantId(event.target.value)} placeholder="active tenant" />
|
||||
) : (
|
||||
<ReferenceSelect
|
||||
value={tenantId}
|
||||
onChange={(value) => {
|
||||
setTenantId(value);
|
||||
setChangeRequestId("");
|
||||
}}
|
||||
provider={tenantProvider}
|
||||
aria-label={ADMIN_INTERFACE_I18N.tenantPickerLabel}
|
||||
placeholder={ADMIN_INTERFACE_I18N.selectTenant}
|
||||
disabled={Boolean(busy)}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="wide">
|
||||
<FormField label="i18n:govoplan-admin.change_request_id.96ee3239" documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
|
||||
{manualReferences ? (
|
||||
<input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." />
|
||||
) : (
|
||||
<ReferenceSelect
|
||||
value={changeRequestId}
|
||||
onChange={(value) => setChangeRequestId(value)}
|
||||
provider={changeRequestProvider}
|
||||
aria-label={ADMIN_INTERFACE_I18N.requestPickerLabel}
|
||||
placeholder={ADMIN_INTERFACE_I18N.selectEligibleRequest}
|
||||
emptyText={ADMIN_INTERFACE_I18N.noEligibleRequests}
|
||||
disabled={Boolean(busy)}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</div>
|
||||
<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)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button>
|
||||
<Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !parsedPackage.value ? ADMIN_INTERFACE_I18N.packageRequired : undefined}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button>
|
||||
<Button variant="primary" onClick={() => setConfirmApply(true)} disabled={!canWrite || !canRun || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
|
||||
<Button onClick={() => void exportAccessPackage()} disabled={Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : undefined}><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>
|
||||
<ConfirmDialog
|
||||
open={confirmApply}
|
||||
title={ADMIN_INTERFACE_I18N.applyTitle}
|
||||
message={ADMIN_INTERFACE_I18N.applyMessage}
|
||||
confirmLabel={ADMIN_INTERFACE_I18N.applyConfirm}
|
||||
busy={busy === "apply"}
|
||||
onCancel={() => setConfirmApply(false)}
|
||||
onConfirm={() => {
|
||||
setConfirmApply(false);
|
||||
void applyPackage();
|
||||
}}
|
||||
/>
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
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>;
|
||||
const columns: DataGridColumn<ConfigurationPackageDiagnostic>[] = [
|
||||
{ id: "severity", header: "i18n:govoplan-admin.severity.de314fa0", width: 130, sortable: true, filterable: true, value: (item) => item.severity, render: (item) => <StatusBadge status={diagnosticTone(item.severity)} label={item.severity} /> },
|
||||
{ id: "code", header: "i18n:govoplan-admin.code.adac6937", width: 190, sortable: true, filterable: true, value: (item) => item.code, render: (item) => <code>{item.code}</code> },
|
||||
{ id: "owner", header: "i18n:govoplan-admin.owner.89ff3122", width: 150, sortable: true, filterable: true, value: (item) => item.module_id || "-", render: (item) => item.module_id || "-" },
|
||||
{ id: "object", header: "i18n:govoplan-admin.object.2883f191", width: 180, sortable: true, filterable: true, value: (item) => item.object_ref || "-", render: (item) => item.object_ref || "-" },
|
||||
{ id: "message", header: "i18n:govoplan-admin.message.68f4145f", width: "minmax(260px, 1fr)", minWidth: 220, resizable: true, filterable: true, value: (item) => `${item.message} ${item.resolution || ""}`, render: (item) => <div>{item.message}{item.resolution ? <span className="muted block">{item.resolution}</span> : null}</div> }
|
||||
];
|
||||
return (
|
||||
<DataGrid
|
||||
id="admin-configuration-package-diagnostics"
|
||||
rows={diagnostics}
|
||||
columns={columns}
|
||||
getRowKey={(item, index) => `${item.code}-${index}`}
|
||||
/>);
|
||||
|
||||
}
|
||||
|
||||
function RequiredData({ items }: {items: ConfigurationPackageRequiredData[];}) {
|
||||
if (items.length === 0) return null;
|
||||
const columns: DataGridColumn<ConfigurationPackageRequiredData>[] = [
|
||||
{ id: "key", header: "i18n:govoplan-admin.key.c67dd20e", width: "minmax(200px, 1fr)", minWidth: 170, resizable: true, sortable: true, filterable: true, value: (item) => item.key, render: (item) => <code>{item.key}</code> },
|
||||
{ id: "label", header: "i18n:govoplan-admin.label.74341e3c", width: "minmax(180px, 1fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (item) => item.label },
|
||||
{ id: "type", header: "i18n:govoplan-admin.type.3deb7456", width: 140, sortable: true, filterable: true, value: (item) => item.data_type },
|
||||
{ id: "required", header: "i18n:govoplan-admin.required.eed6bfb4", width: 110, sortable: true, value: (item) => item.required, render: (item) => item.required ? "yes" : "no" },
|
||||
{ id: "secret", header: "i18n:govoplan-admin.secret.f4e7a874", width: 100, sortable: true, value: (item) => item.secret, render: (item) => item.secret ? "yes" : "no" }
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<h3>i18n:govoplan-admin.required_data.1b1c1b34</h3>
|
||||
<DataGrid id="admin-configuration-package-required-data" rows={items} columns={columns} getRowKey={(item) => item.key} />
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
function PackagePlan({ items }: {items: ConfigurationPackagePlanItem[];}) {
|
||||
if (items.length === 0) return <p className="muted">i18n:govoplan-admin.no_plan_items.7108c582</p>;
|
||||
const columns: DataGridColumn<ConfigurationPackagePlanItem>[] = [
|
||||
{ id: "action", header: "i18n:govoplan-admin.action.97c89a4d", width: 130, sortable: true, filterable: true, value: (item) => item.action, render: (item) => <StatusBadge status={planTone(item.action)} label={item.action} /> },
|
||||
{ id: "module", header: "i18n:govoplan-admin.module.b8ff0289", width: 170, sortable: true, filterable: true, value: (item) => item.module_id },
|
||||
{ id: "fragment", header: "i18n:govoplan-admin.fragment.3f19d616", width: 170, sortable: true, filterable: true, value: (item) => item.fragment_type },
|
||||
{ id: "id", header: "i18n:govoplan-admin.id.474ae526", width: 180, sortable: true, filterable: true, value: (item) => item.fragment_id || "-", render: (item) => item.fragment_id || "-" },
|
||||
{ id: "summary", header: "i18n:govoplan-admin.summary.12b71c3e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.summary || "-", render: (item) => item.summary || "-" }
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<h3>i18n:govoplan-admin.plan.ae2f98a0</h3>
|
||||
<DataGrid id="admin-configuration-package-plan" rows={items} columns={columns} getRowKey={(item, index) => `${item.module_id}-${item.fragment_type}-${item.fragment_id ?? index}`} />
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
function ReferenceMap({ title, refs }: {title: string;refs: Record<string, string>;}) {
|
||||
const entries = Object.entries(refs).map(([key, value]) => ({ key, value }));
|
||||
if (entries.length === 0) return null;
|
||||
const columns: DataGridColumn<{key: string;value: string;}>[] = [
|
||||
{ id: "key", header: "i18n:govoplan-admin.key.c67dd20e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (item) => item.key, render: (item) => <code>{item.key}</code> },
|
||||
{ id: "value", header: "Value", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (item) => item.value }
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<DataGrid id={`admin-configuration-package-refs-${title}`} rows={entries} columns={columns} getRowKey={(item) => item.key} />
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
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,10 @@ import {
|
||||
type GovernanceAssignment,
|
||||
type GovernanceTemplateItem,
|
||||
type PermissionItem,
|
||||
type TenantAdminItem
|
||||
} from "../../api/admin";
|
||||
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels } from "@govoplan/core-webui";
|
||||
type TenantAdminItem } from
|
||||
"../../api/admin";
|
||||
import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N, mutationDisabledReason } from "./interfacePatterns";
|
||||
|
||||
const emptyDraft = {
|
||||
slug: "",
|
||||
@@ -35,12 +36,12 @@ export default function GovernanceTemplatesPanel({
|
||||
kind,
|
||||
canWrite,
|
||||
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 [tenants, setTenants] = useState<TenantAdminItem[]>([]);
|
||||
const [permissions, setPermissions] = useState<PermissionItem[]>([]);
|
||||
@@ -48,46 +49,64 @@ export default function GovernanceTemplatesPanel({
|
||||
const [viewing, setViewing] = useState<GovernanceTemplateItem | null>(null);
|
||||
const [deleting, setDeleting] = useState<GovernanceTemplateItem | null>(null);
|
||||
const [draft, setDraft] = useState(emptyDraft);
|
||||
const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft));
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const dirty = editing !== null && draftKey(draft) !== savedDraftKey;
|
||||
const permissionsByScope = useMemo(() => new Map(permissions.map((permission) => [permission.scope, permission])), [permissions]);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: save,
|
||||
onDiscard: closeEditor
|
||||
});
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const [nextItems, nextTenants, nextPermissions] = await Promise.all([
|
||||
fetchGovernanceTemplates(settings, kind),
|
||||
fetchTenants(settings),
|
||||
kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])
|
||||
]);
|
||||
fetchGovernanceTemplates(settings, kind),
|
||||
fetchTenants(settings),
|
||||
kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])]
|
||||
);
|
||||
setItems(nextItems);
|
||||
setTenants(nextTenants);
|
||||
setPermissions(nextPermissions.filter((item) => item.level === "tenant"));
|
||||
} catch (err) { setError(adminErrorMessage(err)); }
|
||||
finally { setLoading(false); }
|
||||
} catch (err) {setError(adminErrorMessage(err));} finally
|
||||
{setLoading(false);}
|
||||
}
|
||||
|
||||
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl, kind]);
|
||||
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl, kind]);
|
||||
|
||||
function openCreate() {
|
||||
setDraft(emptyDraft);
|
||||
setSavedDraftKey(draftKey(emptyDraft));
|
||||
setEditing("new");
|
||||
}
|
||||
|
||||
function openEdit(item: GovernanceTemplateItem) {
|
||||
setDraft({
|
||||
const nextDraft = {
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
description: item.description || "",
|
||||
isActive: item.is_active,
|
||||
permissions: item.permissions,
|
||||
assignments: item.assignments
|
||||
});
|
||||
};
|
||||
setDraft(nextDraft);
|
||||
setSavedDraftKey(draftKey(nextDraft));
|
||||
setEditing(item);
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
setEditing(null);
|
||||
setDraft(emptyDraft);
|
||||
setSavedDraftKey(draftKey(emptyDraft));
|
||||
}
|
||||
|
||||
function assignmentMode(tenantId: string): "none" | "available" | "required" {
|
||||
return draft.assignments.find((item) => item.tenant_id === tenantId)?.mode ?? "none";
|
||||
}
|
||||
@@ -95,13 +114,13 @@ export default function GovernanceTemplatesPanel({
|
||||
function setAssignment(tenantId: string, mode: "none" | "available" | "required") {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
assignments: mode === "none"
|
||||
? current.assignments.filter((item) => item.tenant_id !== tenantId)
|
||||
: [...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }]
|
||||
assignments: mode === "none" ?
|
||||
current.assignments.filter((item) => item.tenant_id !== tenantId) :
|
||||
[...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }]
|
||||
}));
|
||||
}
|
||||
|
||||
async function save() {
|
||||
async function save(): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -115,7 +134,7 @@ export default function GovernanceTemplatesPanel({
|
||||
is_active: draft.isActive,
|
||||
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) {
|
||||
await updateGovernanceTemplate(settings, editing.id, {
|
||||
name: draft.name,
|
||||
@@ -124,13 +143,14 @@ export default function GovernanceTemplatesPanel({
|
||||
is_active: draft.isActive,
|
||||
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);
|
||||
await load();
|
||||
await onAuthRefresh();
|
||||
} catch (err) { setError(adminErrorMessage(err)); }
|
||||
finally { setBusy(false); }
|
||||
return true;
|
||||
} catch (err) {setError(adminErrorMessage(err));return false;} finally
|
||||
{setBusy(false);}
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
@@ -139,47 +159,47 @@ export default function GovernanceTemplatesPanel({
|
||||
setError("");
|
||||
try {
|
||||
await deleteGovernanceTemplate(settings, deleting.id);
|
||||
setSuccess(`${deleting.name} deleted.`);
|
||||
setSuccess(i18nMessage("i18n:govoplan-admin.value_deleted.3c4bf574", { value0: deleting.name }));
|
||||
setDeleting(null);
|
||||
await load();
|
||||
await onAuthRefresh();
|
||||
} catch (err) { setError(adminErrorMessage(err)); }
|
||||
finally { setBusy(false); }
|
||||
} catch (err) {setError(adminErrorMessage(err));} finally
|
||||
{setBusy(false);}
|
||||
}
|
||||
|
||||
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,
|
||||
value: (row) => `${row.name} ${row.slug}`,
|
||||
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,
|
||||
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) => {
|
||||
const tenant = tenants.find((item) => item.id === assignment.tenant_id);
|
||||
return `${tenant?.name || assignment.tenant_id} (${assignment.mode})`;
|
||||
}).join(", ") : "—"
|
||||
},
|
||||
...(kind === "role" ? [{
|
||||
id: "permissions", header: "Permissions", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const,
|
||||
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: "actions", header: "Actions", width: 150, sticky: "end", resizable: false, align: "right",
|
||||
render: (row) => <div className="admin-icon-actions">
|
||||
<AdminIconButton label={`Inspect ${row.name}`} icon={<Search />} onClick={() => setViewing(row)} />
|
||||
<AdminIconButton label={`Edit ${row.name}`} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canWrite} />
|
||||
<AdminIconButton label={`Delete ${row.name}`} icon={<Trash2 />} variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} />
|
||||
</div>
|
||||
}
|
||||
], [canWrite, kind, tenants]);
|
||||
{
|
||||
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}`,
|
||||
render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div>
|
||||
},
|
||||
{
|
||||
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(", ") || "—",
|
||||
render: (row) => row.assignments.length ? row.assignments.map((assignment) => {
|
||||
const tenant = tenants.find((item) => item.id === assignment.tenant_id);
|
||||
return i18nMessage("i18n:govoplan-admin.value_value.c189e8bc", { value0: tenant?.name || assignment.tenant_id, value1: assignment.mode });
|
||||
}).join(", ") : "—"
|
||||
},
|
||||
...(kind === "role" ? [{
|
||||
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
|
||||
}] : []),
|
||||
{ id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
|
||||
{
|
||||
id: "actions", header: "i18n:govoplan-admin.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right",
|
||||
render: (row) => <TableActionGroup actions={[
|
||||
{ id: "inspect", label: i18nMessage("i18n:govoplan-admin.inspect_value.9d5d1071", { value0: row.name }), icon: <Search />, onClick: () => setViewing(row) },
|
||||
{ id: "edit", label: i18nMessage("i18n:govoplan-admin.edit_value.fad75899", { value0: row.name }), icon: <Pencil />, disabled: !canWrite, disabledReason: !canWrite ? ADMIN_INTERFACE_I18N.governanceWriteRequired : undefined, onClick: () => openEdit(row) },
|
||||
{ id: "delete", label: i18nMessage("i18n:govoplan-admin.delete_value.4d18989e", { value0: row.name }), icon: <Trash2 />, variant: "danger", disabled: !canWrite, disabledReason: !canWrite ? ADMIN_INTERFACE_I18N.governanceWriteRequired : undefined, onClick: () => setDeleting(row) }
|
||||
]} />
|
||||
}],
|
||||
[canWrite, kind, tenants]);
|
||||
|
||||
const title = kind === "group" ? "Central groups" : "Tenant roles";
|
||||
const description = kind === "group"
|
||||
? "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local."
|
||||
: "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.";
|
||||
const title = kind === "group" ? "i18n:govoplan-admin.central_groups.5c9b5b66" : "i18n:govoplan-admin.tenant_roles.51aca82d";
|
||||
const description = kind === "group" ?
|
||||
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9" :
|
||||
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -189,41 +209,73 @@ export default function GovernanceTemplatesPanel({
|
||||
loading={loading}
|
||||
error={error}
|
||||
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={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>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} disabledReason={!canWrite ? ADMIN_INTERFACE_I18N.governanceWriteRequired : undefined} /></>}>
|
||||
|
||||
<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>
|
||||
</AdminPageLayout>
|
||||
|
||||
<Dialog
|
||||
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)}
|
||||
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} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, complete: Boolean(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">
|
||||
<FormField label="Name"><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="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="Description"><textarea rows={3} value={draft.description} onChange={(event) => setDraft({ ...draft, description: 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="i18n:govoplan-admin.slug.094da9b9"><input value={draft.slug} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></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="i18n:govoplan-admin.description.55f8ebc8"><textarea rows={3} value={draft.description} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||
</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">
|
||||
<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">
|
||||
{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>
|
||||
<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>
|
||||
</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>}>
|
||||
{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>}
|
||||
<Dialog open={Boolean(viewing)} title={viewing?.name || "i18n:govoplan-admin.template_details.d5d75e4d"} onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-admin.close.bbfa773e</Button>}>
|
||||
{viewing && <dl className="admin-details-grid"><div><dt>i18n:govoplan-admin.kind.e00ac23f</dt><dd>{viewing.kind}</dd></div><div><dt>i18n:govoplan-admin.slug.094da9b9</dt><dd>{viewing.slug}</dd></div><div><dt>i18n:govoplan-admin.status.bae7d5be</dt><dd>{viewing.is_active ? "i18n:govoplan-admin.active.a733b809" : "i18n:govoplan-admin.inactive.09af574c"}</dd></div><div><dt>i18n:govoplan-admin.tenants.1f7ae776</dt><dd>{viewing.assignments.length || "i18n:govoplan-admin.none.6eef6648"}</dd></div><div><dt>i18n:govoplan-admin.description.55f8ebc8</dt><dd>{viewing.description || "—"}</dd></div><div><dt>i18n:govoplan-admin.permissions.d06d5557</dt><dd>{viewing.permissions.length ? <PermissionDetails scopes={viewing.permissions} permissionsByScope={permissionsByScope} /> : "—"}</dd></div></dl>}
|
||||
</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);
|
||||
}
|
||||
|
||||
function PermissionDetails({ scopes, permissionsByScope }: {scopes: string[];permissionsByScope: ReadonlyMap<string, PermissionItem>;}) {
|
||||
const groups = groupPermissionScopes(scopes, permissionsByScope);
|
||||
return (
|
||||
<div className="admin-permission-details">
|
||||
{groups.map((group) => <section key={group.module}>
|
||||
<strong>{group.module}</strong>
|
||||
<ul>
|
||||
{group.permissions.map((permission) => <li key={permission.scope}>
|
||||
<span>{permission.label}</span>
|
||||
<code>{permission.scope}</code>
|
||||
</li>)}
|
||||
</ul>
|
||||
</section>)}
|
||||
</div>);
|
||||
}
|
||||
|
||||
function groupPermissionScopes(scopes: string[], permissionsByScope: ReadonlyMap<string, PermissionItem>) {
|
||||
const groups = new Map<string, {scope: string;label: string}[]>();
|
||||
for (const scope of [...scopes].sort()) {
|
||||
const moduleId = scope.split(":", 1)[0] || "other";
|
||||
const permission = permissionsByScope.get(scope);
|
||||
const group = groups.get(moduleId) ?? [];
|
||||
group.push({ scope, label: permission?.label || scope });
|
||||
groups.set(moduleId, group);
|
||||
}
|
||||
return [...groups.entries()].map(([module, permissions]) => ({ module, permissions }));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,11 @@ import { Button } from "@govoplan/core-webui";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { FormField } from "@govoplan/core-webui";
|
||||
import { ToggleSwitch } from "@govoplan/core-webui";
|
||||
import { fetchSystemSettings, updateSystemSettings, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsItem } from "../../api/admin";
|
||||
import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui";
|
||||
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin";
|
||||
import { AdminPageLayout, AdminSelectionList, DocumentationHelpLink, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N, mutationDisabledReason } from "./interfacePatterns";
|
||||
|
||||
const DELTA_KEY = "admin:system-settings";
|
||||
|
||||
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
|
||||
store_raw_campaign_json: true,
|
||||
@@ -35,86 +38,212 @@ const fallback: SystemSettingsItem = {
|
||||
allow_tenant_api_keys: true,
|
||||
privacy_retention_policy: defaultPrivacyPolicy,
|
||||
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: {}
|
||||
};
|
||||
|
||||
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 [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(fallback);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = 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() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const loaded = await fetchSystemSettings(settings);
|
||||
setDraft(loaded);
|
||||
const wasDirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
|
||||
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)); }
|
||||
finally { setLoading(false); }
|
||||
catch (err) {setError(adminErrorMessage(err));} finally
|
||||
{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);
|
||||
setError("");
|
||||
try {
|
||||
setDraft(await updateSystemSettings(settings, {
|
||||
const saved = await updateSystemSettings(settings, {
|
||||
default_locale: draft.default_locale,
|
||||
allow_tenant_custom_groups: draft.allow_tenant_custom_groups,
|
||||
allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
|
||||
allow_tenant_api_keys: draft.allow_tenant_api_keys,
|
||||
maintenance_mode: draft.maintenance_mode
|
||||
}));
|
||||
setSuccess("System settings saved.");
|
||||
} catch (err) { setError(adminErrorMessage(err)); }
|
||||
finally { setBusy(false); }
|
||||
maintenance_mode: draft.maintenance_mode,
|
||||
available_languages: draft.available_languages,
|
||||
enabled_language_codes: draft.enabled_language_codes
|
||||
});
|
||||
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 setEnabledLanguages(selected: string[]) {
|
||||
const enabled = new Set(selected);
|
||||
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 (
|
||||
<AdminPageLayout
|
||||
title="System general settings"
|
||||
description="Instance-wide defaults and tenant governance capabilities. Retention policy management has its own system section."
|
||||
title="i18n:govoplan-admin.system_general_settings.7cd662ed"
|
||||
description="i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97"
|
||||
loading={loading}
|
||||
error={error}
|
||||
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={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}>
|
||||
|
||||
<div className="admin-settings-form">
|
||||
<Card title="Defaults for newly created tenants">
|
||||
<FormField label="Default locale"><input value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} /></FormField>
|
||||
<Card title="i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd">
|
||||
<FormField label="i18n:govoplan-admin.default_locale.b99d021f" documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
|
||||
<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 title="Tenant administration capabilities">
|
||||
<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" />
|
||||
<ToggleSwitch checked={draft.allow_tenant_custom_roles} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_roles: checked })} label="Allow tenant-defined roles by default" />
|
||||
<ToggleSwitch checked={draft.allow_tenant_api_keys} onChange={(checked) => setDraft({ ...draft, allow_tenant_api_keys: checked })} label="Allow tenant API keys by default" />
|
||||
<Card title="i18n:govoplan-admin.language_packages">
|
||||
<AdminSelectionList
|
||||
options={draft.available_languages.map((item) => ({ id: item.code, label: item.code.toUpperCase(), description: languageOptionLabel(item), disabled: !canWrite || busy || item.code === draft.default_locale }))}
|
||||
selected={draft.enabled_language_codes}
|
||||
onChange={setEnabledLanguages}
|
||||
/>
|
||||
<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} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, complete: Boolean(normalizeLanguageCode(packageDraft.code) && packageDraft.label.trim()) })}>i18n:govoplan-admin.install_language_package</Button>
|
||||
</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>
|
||||
<p className="muted small-note">i18n:govoplan-admin.language_packages_help</p>
|
||||
</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 })} disabled={!canWrite || busy} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined} 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 })} disabled={!canWrite || busy} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined} 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 })} disabled={!canWrite || busy} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined} 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">
|
||||
<ToggleSwitch
|
||||
checked={draft.maintenance_mode.enabled}
|
||||
disabled={!canWrite || !canAccessMaintenance}
|
||||
disabled={!canWrite || !canAccessMaintenance || busy}
|
||||
help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !canAccessMaintenance ? ADMIN_INTERFACE_I18N.maintenanceAuthorityRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined}
|
||||
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>
|
||||
<FormField label="Maintenance message">
|
||||
<FormField label="i18n:govoplan-admin.maintenance_message.ca62571f" help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : undefined} documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={draft.maintenance_mode.message ?? ""}
|
||||
disabled={!canWrite}
|
||||
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })}
|
||||
/>
|
||||
disabled={!canWrite || busy}
|
||||
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })} />
|
||||
|
||||
</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>
|
||||
</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 } : {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AdminPageLayout,
|
||||
adminErrorMessage,
|
||||
Button,
|
||||
Card,
|
||||
dispatchPlatformModulesChanged,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
MetricCard,
|
||||
SearchableSelect,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type SearchableSelectOption
|
||||
} from "@govoplan/core-webui";
|
||||
import { RefreshCw, Save, Undo2 } from "lucide-react";
|
||||
import {
|
||||
fetchSystemTenantModules,
|
||||
fetchTenantModuleTargets,
|
||||
fetchTenantModules,
|
||||
updateSystemTenantModules,
|
||||
updateTenantModules,
|
||||
type TenantModuleAvailability,
|
||||
type TenantModuleEntitlementResponse,
|
||||
type TenantModuleTarget
|
||||
} from "../../api/admin";
|
||||
|
||||
type Props = {
|
||||
settings: ApiSettings;
|
||||
scope: "system" | "tenant";
|
||||
canWrite: boolean;
|
||||
};
|
||||
|
||||
type Draft = {
|
||||
available: Set<string>;
|
||||
forced: Set<string>;
|
||||
enabled: Set<string>;
|
||||
};
|
||||
|
||||
const DOCUMENTATION = {
|
||||
contextId: "admin.tenant-modules",
|
||||
documentationType: "admin" as const
|
||||
};
|
||||
|
||||
export default function TenantModuleManagementPanel({ settings, scope, canWrite }: Props) {
|
||||
const [targets, setTargets] = useState<TenantModuleTarget[]>([]);
|
||||
const [targetId, setTargetId] = useState("");
|
||||
const [state, setState] = useState<TenantModuleEntitlementResponse | null>(null);
|
||||
const [draft, setDraft] = useState<Draft | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const targetOptions = useMemo<SearchableSelectOption[]>(() => targets.map((target) => ({
|
||||
value: target.id,
|
||||
label: target.name,
|
||||
description: `${target.slug}${target.is_active ? "" : " - inactive"}`,
|
||||
searchText: `${target.name} ${target.slug}`
|
||||
})), [targets]);
|
||||
|
||||
const dirty = Boolean(state && draft && (
|
||||
!sameSet(draft.available, new Set(state.available_modules))
|
||||
|| !sameSet(draft.forced, new Set(state.forced_modules))
|
||||
|| !sameSet(draft.enabled, new Set(state.selected_modules))
|
||||
));
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: save,
|
||||
onDiscard: discard
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void initialize();
|
||||
}, [scope, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
async function initialize() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
if (scope === "system") {
|
||||
const loadedTargets = await fetchTenantModuleTargets(settings);
|
||||
setTargets(loadedTargets);
|
||||
const nextTarget = loadedTargets.some((item) => item.id === targetId)
|
||||
? targetId
|
||||
: loadedTargets[0]?.id ?? "";
|
||||
setTargetId(nextTarget);
|
||||
if (nextTarget) {
|
||||
await load(nextTarget, false);
|
||||
} else {
|
||||
setState(null);
|
||||
setDraft(null);
|
||||
}
|
||||
} else {
|
||||
setTargets([]);
|
||||
setTargetId("");
|
||||
await load(undefined, false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
setState(null);
|
||||
setDraft(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function load(nextTargetId = targetId, manageLoading = true) {
|
||||
if (scope === "system" && !nextTargetId) return;
|
||||
if (manageLoading) setLoading(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const loaded = scope === "system"
|
||||
? await fetchSystemTenantModules(settings, nextTargetId)
|
||||
: await fetchTenantModules(settings);
|
||||
setState(loaded);
|
||||
setDraft(draftFromState(loaded));
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
if (manageLoading) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTarget(nextTargetId: string) {
|
||||
if (!nextTargetId || nextTargetId === targetId) return;
|
||||
setTargetId(nextTargetId);
|
||||
await load(nextTargetId);
|
||||
}
|
||||
|
||||
function discard() {
|
||||
if (state) setDraft(draftFromState(state));
|
||||
setError("");
|
||||
setSuccess("");
|
||||
}
|
||||
|
||||
async function save(): Promise<boolean> {
|
||||
if (!state || !draft || !dirty) return true;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const loaded = scope === "system"
|
||||
? await updateSystemTenantModules(settings, targetId, {
|
||||
available_modules: sorted(draft.available),
|
||||
forced_modules: sorted(draft.forced),
|
||||
enabled_modules: sorted(draft.enabled),
|
||||
expected_revision: state.revision
|
||||
})
|
||||
: await updateTenantModules(settings, {
|
||||
enabled_modules: sorted(draft.enabled),
|
||||
expected_revision: state.revision
|
||||
});
|
||||
setState(loaded);
|
||||
setDraft(draftFromState(loaded));
|
||||
setSuccess("Tenant module selection saved.");
|
||||
dispatchPlatformModulesChanged();
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function setAvailability(moduleId: string, availability: TenantModuleAvailability) {
|
||||
setDraft((current) => {
|
||||
if (!current) return current;
|
||||
const next = cloneDraft(current);
|
||||
if (availability === "unavailable") {
|
||||
next.available.delete(moduleId);
|
||||
next.forced.delete(moduleId);
|
||||
next.enabled.delete(moduleId);
|
||||
} else {
|
||||
next.available.add(moduleId);
|
||||
if (availability === "forced") next.forced.add(moduleId);
|
||||
else next.forced.delete(moduleId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function setEnabled(moduleId: string, enabled: boolean) {
|
||||
setDraft((current) => {
|
||||
if (!current) return current;
|
||||
const next = cloneDraft(current);
|
||||
if (enabled) next.enabled.add(moduleId);
|
||||
else next.enabled.delete(moduleId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
const labels = scope === "system"
|
||||
? {
|
||||
title: "Tenant modules",
|
||||
description: "Set a tenant's module ceiling, forced modules, and current selection."
|
||||
}
|
||||
: {
|
||||
title: "Modules",
|
||||
description: "Enable or disable modules made available to this tenant by system policy."
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminPageLayout
|
||||
title={labels.title}
|
||||
description={labels.description}
|
||||
loading={loading}
|
||||
error={error}
|
||||
success={success}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
title="Reload saved module policy"
|
||||
aria-label="Reload saved module policy"
|
||||
onClick={() => void load()}
|
||||
disabled={loading || busy || (scope === "system" && !targetId)}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
</Button>
|
||||
<Button onClick={discard} disabled={!dirty || busy}>
|
||||
<Undo2 size={16} /> Discard
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => void save()} disabled={!canWrite || !dirty || busy}>
|
||||
<Save size={16} /> {busy ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
<DocumentationHelpLink reference={DOCUMENTATION} label="Open module governance documentation" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
{scope === "system" && (
|
||||
<FormField label="Tenant" documentation={DOCUMENTATION}>
|
||||
<SearchableSelect
|
||||
value={targetId}
|
||||
options={targetOptions}
|
||||
onChange={(value) => void selectTarget(value)}
|
||||
placeholder="Select tenant"
|
||||
searchPlaceholder="Search tenants..."
|
||||
disabled={loading || busy || targetOptions.length === 0}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
{state && draft && (
|
||||
<>
|
||||
<div className="metric-grid module-management-metrics">
|
||||
<MetricCard label="Available" value={draft.available.size} tone="info" />
|
||||
<MetricCard label="Forced" value={draft.forced.size} tone="warning" />
|
||||
<MetricCard label="Selected" value={draft.enabled.size} />
|
||||
<MetricCard label="Effective now" value={state.effective_modules.length} tone="good" />
|
||||
</div>
|
||||
|
||||
{state.diagnostics.map((diagnostic) => (
|
||||
<DismissibleAlert key={`${diagnostic.code}:${diagnostic.message}`} tone="warning" compact>
|
||||
{diagnostic.message}
|
||||
</DismissibleAlert>
|
||||
))}
|
||||
|
||||
<Card title={scope === "system" ? "Module policy and tenant selection" : "Tenant module selection"}>
|
||||
<div className="module-management-list">
|
||||
{state.modules.map((module) => {
|
||||
const availability = draftAvailability(draft, module.id);
|
||||
const effectiveSelection = draft.enabled.has(module.id) || availability === "forced" || module.derived_dependency;
|
||||
const selectionLocked = availability !== "available" || module.derived_dependency;
|
||||
return (
|
||||
<div className={`module-management-row${dirtyModule(state, draft, module.id) ? " pending" : ""}`} key={module.id}>
|
||||
<div className="module-management-main">
|
||||
<div className="module-management-title">
|
||||
<strong>{module.name}</strong>
|
||||
<code>{module.id}</code>
|
||||
<StatusBadge
|
||||
status={module.runtime_active ? "success" : "neutral"}
|
||||
label={module.runtime_active ? "Runtime active" : "Runtime inactive"}
|
||||
/>
|
||||
{module.effective && <StatusBadge status="info" label="Effective" />}
|
||||
</div>
|
||||
<div className="module-management-details">
|
||||
<span>{module.dependencies.length ? `Requires ${module.dependencies.join(", ")}` : "No module dependencies"}</span>
|
||||
{module.reason && <span>{module.reason}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-management-toggle">
|
||||
{scope === "system" && (
|
||||
<label>
|
||||
<span>System policy</span>
|
||||
<select
|
||||
value={availability}
|
||||
onChange={(event) => setAvailability(module.id, event.target.value as TenantModuleAvailability)}
|
||||
disabled={!canWrite || busy || (module.id === "access" || module.id === "admin")}
|
||||
>
|
||||
<option value="unavailable">Unavailable</option>
|
||||
<option value="available">Available</option>
|
||||
<option value="forced">Forced</option>
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<ToggleSwitch
|
||||
label="Enabled for tenant"
|
||||
checked={effectiveSelection}
|
||||
onChange={(checked) => setEnabled(module.id, checked)}
|
||||
disabled={!canWrite || busy || selectionLocked}
|
||||
help={module.reason || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && !state && (
|
||||
<DismissibleAlert tone="info" dismissible={false}>
|
||||
{scope === "system" ? "No tenant is available for module policy." : "Module policy is unavailable."}
|
||||
</DismissibleAlert>
|
||||
)}
|
||||
</AdminPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function draftFromState(state: TenantModuleEntitlementResponse): Draft {
|
||||
return {
|
||||
available: new Set(state.available_modules),
|
||||
forced: new Set(state.forced_modules),
|
||||
enabled: new Set(state.selected_modules)
|
||||
};
|
||||
}
|
||||
|
||||
function cloneDraft(draft: Draft): Draft {
|
||||
return {
|
||||
available: new Set(draft.available),
|
||||
forced: new Set(draft.forced),
|
||||
enabled: new Set(draft.enabled)
|
||||
};
|
||||
}
|
||||
|
||||
function draftAvailability(draft: Draft, moduleId: string): TenantModuleAvailability {
|
||||
if (draft.forced.has(moduleId)) return "forced";
|
||||
if (draft.available.has(moduleId)) return "available";
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
function dirtyModule(state: TenantModuleEntitlementResponse, draft: Draft, moduleId: string): boolean {
|
||||
return state.available_modules.includes(moduleId) !== draft.available.has(moduleId)
|
||||
|| state.forced_modules.includes(moduleId) !== draft.forced.has(moduleId)
|
||||
|| state.selected_modules.includes(moduleId) !== draft.enabled.has(moduleId);
|
||||
}
|
||||
|
||||
function sameSet(left: Set<string>, right: Set<string>): boolean {
|
||||
return left.size === right.size && [...left].every((item) => right.has(item));
|
||||
}
|
||||
|
||||
function sorted(values: Set<string>): string[] {
|
||||
return [...values].sort();
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
filterSearchableSelectOptions,
|
||||
unavailableReferenceOption,
|
||||
type ApiSettings,
|
||||
type ConfigurationReferenceSelectorsUiCapability,
|
||||
type ReferenceOption,
|
||||
type ReferenceOptionProvider
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
fetchConfigurationChanges,
|
||||
fetchTenants,
|
||||
type ConfigurationChangeRequest
|
||||
} from "../../api/admin";
|
||||
|
||||
export const configurationReferenceSelectors: ConfigurationReferenceSelectorsUiCapability = {
|
||||
tenantProvider: createTenantReferenceProvider,
|
||||
changeRequestProvider: createChangeRequestReferenceProvider
|
||||
};
|
||||
|
||||
export function createTenantReferenceProvider(
|
||||
settings: ApiSettings
|
||||
): ReferenceOptionProvider {
|
||||
async function catalogue(signal: AbortSignal): Promise<ReferenceOption[]> {
|
||||
const tenants = await fetchTenants(settings);
|
||||
if (signal.aborted) throw abortError();
|
||||
return tenants.map((tenant) => ({
|
||||
value: tenant.id,
|
||||
label: tenant.name || tenant.slug || tenant.id,
|
||||
description: [
|
||||
tenant.slug,
|
||||
tenant.is_active ? null : "Inactive",
|
||||
tenant.id
|
||||
].filter(Boolean).join(" · "),
|
||||
kind: "tenant",
|
||||
availability: tenant.is_active ? "available" : "inactive",
|
||||
disabled: !tenant.is_active,
|
||||
sourceModule: "tenancy",
|
||||
provenance: {
|
||||
tenantId: tenant.id,
|
||||
active: tenant.is_active
|
||||
}
|
||||
}));
|
||||
}
|
||||
return {
|
||||
async search(query, context) {
|
||||
const options = await catalogue(context.signal);
|
||||
return retainSelected(
|
||||
filterSearchableSelectOptions(options, query, context.limit),
|
||||
options,
|
||||
context.selectedValues
|
||||
);
|
||||
},
|
||||
async resolve(values, context) {
|
||||
const options = await catalogue(context.signal);
|
||||
const byValue = new Map(options.map((option) => [option.value, option]));
|
||||
return values.map(
|
||||
(value) =>
|
||||
byValue.get(value)
|
||||
?? unavailableReferenceOption(value, "Unavailable tenant")
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createChangeRequestReferenceProvider(
|
||||
settings: ApiSettings,
|
||||
{
|
||||
purpose,
|
||||
tenantId
|
||||
}: {
|
||||
purpose: string;
|
||||
tenantId?: string | null;
|
||||
}
|
||||
): ReferenceOptionProvider {
|
||||
async function catalogue(signal: AbortSignal): Promise<{
|
||||
eligible: ReferenceOption[];
|
||||
all: ReferenceOption[];
|
||||
}> {
|
||||
const response = await fetchConfigurationChanges(settings);
|
||||
if (signal.aborted) throw abortError();
|
||||
const matching = response.requests.filter(
|
||||
(request) =>
|
||||
request.key === purpose
|
||||
&& requestTargetsTenant(request, tenantId)
|
||||
);
|
||||
const all = matching.map(changeRequestOption);
|
||||
return {
|
||||
eligible: all.filter((option) => !option.disabled),
|
||||
all
|
||||
};
|
||||
}
|
||||
return {
|
||||
async search(query, context) {
|
||||
const options = await catalogue(context.signal);
|
||||
return retainSelected(
|
||||
filterSearchableSelectOptions(
|
||||
options.eligible,
|
||||
query,
|
||||
context.limit
|
||||
),
|
||||
options.all,
|
||||
context.selectedValues
|
||||
);
|
||||
},
|
||||
async resolve(values, context) {
|
||||
const options = await catalogue(context.signal);
|
||||
const byValue = new Map(
|
||||
options.all.map((option) => [option.value, option])
|
||||
);
|
||||
return values.map(
|
||||
(value) =>
|
||||
byValue.get(value)
|
||||
?? unavailableReferenceOption(
|
||||
value,
|
||||
"Unavailable configuration request"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function changeRequestOption(
|
||||
request: ConfigurationChangeRequest
|
||||
): ReferenceOption {
|
||||
const closed = request.status === "applied" || request.status === "rejected";
|
||||
const eligible = request.status === "approved" && request.dry_run;
|
||||
return {
|
||||
value: request.id,
|
||||
label: request.label || request.key,
|
||||
description: [
|
||||
request.status.split("_").join(" "),
|
||||
request.dry_run ? null : "dry run not recorded",
|
||||
formatTimestamp(request.requested_at),
|
||||
`requested by ${request.requested_by}`,
|
||||
request.id
|
||||
].filter(Boolean).join(" · "),
|
||||
searchText: `${request.id} ${request.requested_by} ${request.status}`,
|
||||
kind: "configuration_change_request",
|
||||
availability: closed
|
||||
? "unavailable"
|
||||
: eligible
|
||||
? "available"
|
||||
: "inactive",
|
||||
disabled: !eligible,
|
||||
sourceModule: "admin",
|
||||
provenance: {
|
||||
purpose: request.key,
|
||||
target: request.target ?? {},
|
||||
requestedBy: request.requested_by,
|
||||
requestedAt: request.requested_at,
|
||||
status: request.status,
|
||||
dryRunRecorded: request.dry_run
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function requestTargetsTenant(
|
||||
request: ConfigurationChangeRequest,
|
||||
tenantId?: string | null
|
||||
): boolean {
|
||||
if (!tenantId) return true;
|
||||
const targetTenant = request.target?.tenant_id;
|
||||
return !targetTenant || String(targetTenant) === tenantId;
|
||||
}
|
||||
|
||||
function retainSelected(
|
||||
matches: readonly ReferenceOption[],
|
||||
catalogue: readonly ReferenceOption[],
|
||||
selectedValues: readonly string[]
|
||||
): ReferenceOption[] {
|
||||
const result = [...matches];
|
||||
const returned = new Set(result.map((option) => option.value));
|
||||
const byValue = new Map(catalogue.map((option) => [option.value, option]));
|
||||
for (const value of selectedValues) {
|
||||
if (returned.has(value)) continue;
|
||||
result.push(
|
||||
byValue.get(value)
|
||||
?? unavailableReferenceOption(value)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatTimestamp(value: string): string {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
}
|
||||
|
||||
function abortError(): DOMException {
|
||||
return new DOMException("The operation was aborted.", "AbortError");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||
|
||||
export const ADMIN_WORKSPACE_DOCUMENTATION = {
|
||||
topicId: "admin.workspace",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const ADMIN_GOVERNANCE_DOCUMENTATION = {
|
||||
topicId: "admin.governance-and-module-lifecycle",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const MODULE_LIFECYCLE_DOCUMENTATION = {
|
||||
topicId: "admin.module-lifecycle-workflow",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const ADMIN_INTERFACE_I18N = {
|
||||
loading: "i18n:govoplan-admin.administration_data_is_loading.6bf3c001",
|
||||
busy: "i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002",
|
||||
writeRequired: "i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003",
|
||||
governanceWriteRequired: "i18n:govoplan-admin.system_governance_write_permission_is_required.6bf3c004",
|
||||
completeRequiredFields: "i18n:govoplan-admin.complete_the_required_fields_before_saving.6bf3c005",
|
||||
noPendingChanges: "i18n:govoplan-admin.make_a_change_before_saving.6bf3c006",
|
||||
validJsonRequired: "i18n:govoplan-admin.provide_valid_package_and_supplied_data_json.6bf3c007",
|
||||
packageRequired: "i18n:govoplan-admin.provide_valid_package_json_before_requesting_approval.6bf3c008",
|
||||
planRequired: "i18n:govoplan-admin.add_at_least_one_valid_plan_item.6bf3c009",
|
||||
savePlanFirst: "i18n:govoplan-admin.save_the_changed_plan_before_queueing_it.6bf3c010",
|
||||
maintenanceAuthorityRequired: "i18n:govoplan-admin.system_maintenance_authority_is_required.6bf3c011",
|
||||
maintenanceRequired: "i18n:govoplan-admin.enable_maintenance_mode_before_this_action.6bf3c012",
|
||||
protectedDefinition: "i18n:govoplan-admin.this_protected_module_cannot_be_changed.6bf3c013",
|
||||
deactivateBeforeUninstall: "i18n:govoplan-admin.deactivate_the_module_before_planning_uninstall.6bf3c014",
|
||||
catalogBlocked: "i18n:govoplan-admin.resolve_the_catalog_license_or_action_blocker_first.6bf3c015",
|
||||
approveTitle: "i18n:govoplan-admin.approve_configuration_change.6bf3c016",
|
||||
approveMessage: "i18n:govoplan-admin.approving_records_your_authority_and_may_unlock_application.6bf3c017",
|
||||
applyTitle: "i18n:govoplan-admin.apply_configuration_package.6bf3c018",
|
||||
applyMessage: "i18n:govoplan-admin.apply_the_current_package_to_the_selected_scope.6bf3c019",
|
||||
applyConfirm: "i18n:govoplan-admin.apply_package.6bf3c020",
|
||||
enterReferencesManually: "i18n:govoplan-admin.enter_reference_ids_manually.6bf3c021",
|
||||
manualReferenceHelp: "i18n:govoplan-admin.use_manual_ids_only_for_intentional_historical_references.6bf3c022",
|
||||
tenantPickerLabel: "i18n:govoplan-admin.configuration_package_tenant.6bf3c023",
|
||||
selectTenant: "i18n:govoplan-admin.select_a_tenant.6bf3c024",
|
||||
requestPickerLabel: "i18n:govoplan-admin.configuration_package_change_request.6bf3c025",
|
||||
selectEligibleRequest: "i18n:govoplan-admin.select_an_eligible_request_optional.6bf3c026",
|
||||
noEligibleRequests: "i18n:govoplan-admin.no_eligible_configuration_requests.6bf3c027",
|
||||
administrationHeading: "i18n:govoplan-admin.administration.b8be3d12",
|
||||
globalHeading: "i18n:govoplan-admin.global.6bf3c028",
|
||||
tenantHeading: "i18n:govoplan-admin.tenant.6bf3c029",
|
||||
groupHeading: "i18n:govoplan-admin.group.171a0606",
|
||||
userHeading: "i18n:govoplan-admin.user.6bf3c030",
|
||||
clearPlanTitle: "i18n:govoplan-admin.clear_saved_module_plan.6bf3c032",
|
||||
clearPlanMessage: "i18n:govoplan-admin.clear_all_saved_module_install_update_and_uninstall_items.6bf3c033",
|
||||
enableMaintenanceTitle: "i18n:govoplan-admin.enable_maintenance_mode.6bf3c034",
|
||||
enableMaintenanceMessage: "i18n:govoplan-admin.restrict_normal_authenticated_access_while_module_work_runs.6bf3c035",
|
||||
cancelRequestTitle: "i18n:govoplan-admin.cancel_installer_request.6bf3c036",
|
||||
cancelRequestMessage: "i18n:govoplan-admin.cancel_the_queued_request_before_the_daemon_accepts_it.6bf3c037",
|
||||
uninstallOnly: "i18n:govoplan-admin.this_option_only_applies_to_uninstall_plans.6bf3c038",
|
||||
installUpdateOnly: "i18n:govoplan-admin.this_option_only_applies_to_install_and_update_plans.6bf3c039"
|
||||
} as const;
|
||||
|
||||
export function mutationDisabledReason({
|
||||
busy,
|
||||
permitted,
|
||||
complete = true,
|
||||
changed = true
|
||||
}: {
|
||||
busy: boolean;
|
||||
permitted: boolean;
|
||||
complete?: boolean;
|
||||
changed?: boolean;
|
||||
}): string | undefined {
|
||||
if (busy) return ADMIN_INTERFACE_I18N.busy;
|
||||
if (!permitted) return ADMIN_INTERFACE_I18N.writeRequired;
|
||||
if (!complete) return ADMIN_INTERFACE_I18N.completeRequiredFields;
|
||||
if (!changed) return ADMIN_INTERFACE_I18N.noPendingChanges;
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
export type ModuleInstallerWorkflowStageId =
|
||||
| "plan"
|
||||
| "preflight"
|
||||
| "queue"
|
||||
| "execute"
|
||||
| "evidence";
|
||||
|
||||
export type ModuleInstallerWorkflowStageState =
|
||||
| "complete"
|
||||
| "current"
|
||||
| "locked"
|
||||
| "blocked"
|
||||
| "failed";
|
||||
|
||||
export type ModuleInstallerWorkflowStage = {
|
||||
id: ModuleInstallerWorkflowStageId;
|
||||
state: ModuleInstallerWorkflowStageState;
|
||||
current: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
export type ModuleInstallerQueueBlock =
|
||||
| "write_access"
|
||||
| "empty_plan"
|
||||
| "invalid_plan"
|
||||
| "unsaved_plan"
|
||||
| "preflight"
|
||||
| "maintenance_mode"
|
||||
| "maintenance_access";
|
||||
|
||||
export type ModuleInstallerWorkflowInput = {
|
||||
planItemCount: number;
|
||||
planDirty: boolean;
|
||||
planValid: boolean;
|
||||
preflightAllowed: boolean | null;
|
||||
maintenanceEnabled: boolean;
|
||||
canWrite: boolean;
|
||||
canAccessMaintenance: boolean;
|
||||
requestStatus?: string | null;
|
||||
runStatus?: string | null;
|
||||
};
|
||||
|
||||
const ACTIVE_STATUSES = new Set([
|
||||
"queued",
|
||||
"pending",
|
||||
"claimed",
|
||||
"starting",
|
||||
"running",
|
||||
"cancelling",
|
||||
"rolling_back"
|
||||
]);
|
||||
|
||||
const FAILED_STATUSES = new Set([
|
||||
"blocked",
|
||||
"cancelled",
|
||||
"failed",
|
||||
"rollback_failed"
|
||||
]);
|
||||
|
||||
export function moduleInstallerQueueBlock(
|
||||
input: ModuleInstallerWorkflowInput
|
||||
): ModuleInstallerQueueBlock | null {
|
||||
if (!input.canWrite) return "write_access";
|
||||
if (input.planItemCount === 0) return "empty_plan";
|
||||
if (!input.planValid) return "invalid_plan";
|
||||
if (input.planDirty) return "unsaved_plan";
|
||||
if (input.preflightAllowed !== true) return "preflight";
|
||||
if (!input.canAccessMaintenance) return "maintenance_access";
|
||||
if (!input.maintenanceEnabled) return "maintenance_mode";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function moduleInstallerWorkflowStages(
|
||||
input: ModuleInstallerWorkflowInput
|
||||
): ModuleInstallerWorkflowStage[] {
|
||||
const queueBlock = moduleInstallerQueueBlock(input);
|
||||
const planReady = input.planItemCount > 0 && input.planValid && !input.planDirty;
|
||||
const preflightReady = planReady && input.preflightAllowed === true;
|
||||
const requestStatus = normalizeStatus(input.requestStatus);
|
||||
const runStatus = normalizeStatus(input.runStatus);
|
||||
const hasRequest = Boolean(requestStatus);
|
||||
const effectiveStatus = runStatus || requestStatus;
|
||||
const active = ACTIVE_STATUSES.has(effectiveStatus);
|
||||
const terminalStatus = hasRequest && !active ? effectiveStatus : "";
|
||||
const terminal = Boolean(terminalStatus);
|
||||
const failed = FAILED_STATUSES.has(terminalStatus);
|
||||
|
||||
if (!planReady) {
|
||||
return stagesAt("plan", input.planValid || input.planItemCount === 0 ? "current" : "blocked");
|
||||
}
|
||||
if (!preflightReady) {
|
||||
return stagesAt("preflight", input.preflightAllowed === false ? "blocked" : "current", ["plan"]);
|
||||
}
|
||||
if (!hasRequest) {
|
||||
return stagesAt("queue", queueBlock ? "blocked" : "current", ["plan", "preflight"]);
|
||||
}
|
||||
if (!terminal) {
|
||||
return stagesAt("execute", "current", ["plan", "preflight", "queue"]);
|
||||
}
|
||||
return stagesAt(
|
||||
"evidence",
|
||||
failed ? "failed" : "current",
|
||||
["plan", "preflight", "queue", "execute"]
|
||||
);
|
||||
}
|
||||
|
||||
export function installerRequestMatchesPlan(
|
||||
planUpdatedAt?: string | null,
|
||||
requestCreatedAt?: string | null
|
||||
): boolean {
|
||||
if (!planUpdatedAt) return true;
|
||||
if (!requestCreatedAt) return false;
|
||||
const planTime = Date.parse(planUpdatedAt);
|
||||
const requestTime = Date.parse(requestCreatedAt);
|
||||
return Number.isFinite(planTime)
|
||||
&& Number.isFinite(requestTime)
|
||||
&& requestTime >= planTime;
|
||||
}
|
||||
|
||||
function stagesAt(
|
||||
currentId: ModuleInstallerWorkflowStageId,
|
||||
currentState: Extract<ModuleInstallerWorkflowStageState, "current" | "blocked" | "failed">,
|
||||
completed: ModuleInstallerWorkflowStageId[] = []
|
||||
): ModuleInstallerWorkflowStage[] {
|
||||
const ids: ModuleInstallerWorkflowStageId[] = [
|
||||
"plan",
|
||||
"preflight",
|
||||
"queue",
|
||||
"execute",
|
||||
"evidence"
|
||||
];
|
||||
const currentIndex = ids.indexOf(currentId);
|
||||
const completedIds = new Set(completed);
|
||||
return ids.map((id, index) => {
|
||||
const current = id === currentId;
|
||||
const state: ModuleInstallerWorkflowStageState = current
|
||||
? currentState
|
||||
: completedIds.has(id)
|
||||
? "complete"
|
||||
: "locked";
|
||||
return {
|
||||
id,
|
||||
state,
|
||||
current,
|
||||
locked: !current && index > currentIndex
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStatus(value?: string | null): string {
|
||||
return value?.trim().toLowerCase().replaceAll("-", "_") ?? "";
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-admin.administration_data_is_loading.6bf3c001": "Administration data is loading.",
|
||||
"i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002": "An administration operation is in progress.",
|
||||
"i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003": "System administration write permission is required.",
|
||||
"i18n:govoplan-admin.system_governance_write_permission_is_required.6bf3c004": "System governance write permission is required.",
|
||||
"i18n:govoplan-admin.complete_the_required_fields_before_saving.6bf3c005": "Complete the required fields before saving.",
|
||||
"i18n:govoplan-admin.make_a_change_before_saving.6bf3c006": "Make a change before saving.",
|
||||
"i18n:govoplan-admin.provide_valid_package_and_supplied_data_json.6bf3c007": "Provide valid package and supplied-data JSON.",
|
||||
"i18n:govoplan-admin.provide_valid_package_json_before_requesting_approval.6bf3c008": "Provide valid package JSON before requesting approval.",
|
||||
"i18n:govoplan-admin.add_at_least_one_valid_plan_item.6bf3c009": "Add at least one valid plan item.",
|
||||
"i18n:govoplan-admin.save_the_changed_plan_before_queueing_it.6bf3c010": "Save the changed plan before queueing it.",
|
||||
"i18n:govoplan-admin.system_maintenance_authority_is_required.6bf3c011": "System maintenance authority is required.",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode_before_this_action.6bf3c012": "Enable maintenance mode before this action.",
|
||||
"i18n:govoplan-admin.this_protected_module_cannot_be_changed.6bf3c013": "This protected module cannot be changed.",
|
||||
"i18n:govoplan-admin.deactivate_the_module_before_planning_uninstall.6bf3c014": "Deactivate the module before planning its uninstall.",
|
||||
"i18n:govoplan-admin.resolve_the_catalog_license_or_action_blocker_first.6bf3c015": "Resolve the catalog, license, or package-action blocker first.",
|
||||
"i18n:govoplan-admin.approve_configuration_change.6bf3c016": "Approve configuration change",
|
||||
"i18n:govoplan-admin.approving_records_your_authority_and_may_unlock_application.6bf3c017": "Approving records your authority and may unlock application of this configuration change.",
|
||||
"i18n:govoplan-admin.apply_configuration_package.6bf3c018": "Apply configuration package",
|
||||
"i18n:govoplan-admin.apply_the_current_package_to_the_selected_scope.6bf3c019": "Apply the current package to the selected scope? This may create or update module-owned configuration.",
|
||||
"i18n:govoplan-admin.apply_package.6bf3c020": "Apply package",
|
||||
"i18n:govoplan-admin.enter_reference_ids_manually.6bf3c021": "Enter reference IDs manually",
|
||||
"i18n:govoplan-admin.use_manual_ids_only_for_intentional_historical_references.6bf3c022": "Use manual IDs only when a package intentionally references a target no longer returned by the live catalog.",
|
||||
"i18n:govoplan-admin.configuration_package_tenant.6bf3c023": "Configuration package tenant",
|
||||
"i18n:govoplan-admin.select_a_tenant.6bf3c024": "Select a tenant",
|
||||
"i18n:govoplan-admin.configuration_package_change_request.6bf3c025": "Configuration package change request",
|
||||
"i18n:govoplan-admin.select_an_eligible_request_optional.6bf3c026": "Select an eligible request (optional)",
|
||||
"i18n:govoplan-admin.no_eligible_configuration_requests.6bf3c027": "No eligible configuration requests.",
|
||||
"i18n:govoplan-admin.global.6bf3c028": "Global",
|
||||
"i18n:govoplan-admin.tenant.6bf3c029": "Tenant",
|
||||
"i18n:govoplan-admin.user.6bf3c030": "User",
|
||||
"i18n:govoplan-admin.request_is_not_pending_approval.6bf3c031": "This request is not pending approval.",
|
||||
"i18n:govoplan-admin.clear_saved_module_plan.6bf3c032": "Clear saved module plan",
|
||||
"i18n:govoplan-admin.clear_all_saved_module_install_update_and_uninstall_items.6bf3c033": "Clear all saved module install, update, and uninstall items?",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode.6bf3c034": "Enable maintenance mode",
|
||||
"i18n:govoplan-admin.restrict_normal_authenticated_access_while_module_work_runs.6bf3c035": "Restrict normal authenticated access while supervised module work runs?",
|
||||
"i18n:govoplan-admin.cancel_installer_request.6bf3c036": "Cancel installer request",
|
||||
"i18n:govoplan-admin.cancel_the_queued_request_before_the_daemon_accepts_it.6bf3c037": "Cancel this queued request before the installer daemon accepts it?",
|
||||
"i18n:govoplan-admin.this_option_only_applies_to_uninstall_plans.6bf3c038": "This option only applies to uninstall plans.",
|
||||
"i18n:govoplan-admin.this_option_only_applies_to_install_and_update_plans.6bf3c039": "This option only applies to install and update plans.",
|
||||
"i18n:govoplan-admin.module_lifecycle_progress.f1a20201": "Module lifecycle progress",
|
||||
"i18n:govoplan-admin.queue_supervised_run_unavailable.f1a20202": "Queueing a supervised run is unavailable",
|
||||
"i18n:govoplan-admin.required_action.f1a20203": "Required action",
|
||||
"i18n:govoplan-admin.who_can_fix_it.f1a20204": "Who can fix it",
|
||||
"i18n:govoplan-admin.where_to_go.f1a20205": "Where to go",
|
||||
"i18n:govoplan-admin.current_operator.f1a20206": "Current operator",
|
||||
"i18n:govoplan-admin.system_administrator.f1a20207": "System administrator",
|
||||
"i18n:govoplan-admin.module_write_access_is_required.f1a20208": "Module write access is required.",
|
||||
"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.acknowledged.d08d7c6d": "Acknowledged",
|
||||
"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.after.695c89be": "After:",
|
||||
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
|
||||
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
|
||||
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add role template",
|
||||
"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.before.a11cf31f": "Before:",
|
||||
"i18n:govoplan-admin.bridge_release.176cf241": "Bridge release",
|
||||
"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.catalog_metadata.9b96c41a": "Catalog metadata",
|
||||
"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.manual.17a486ca": "Manual",
|
||||
"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 role template",
|
||||
"i18n:govoplan-admin.created.0c78dab1": "Created:",
|
||||
"i18n:govoplan-admin.created.accf40c8": "Created",
|
||||
"i18n:govoplan-admin.current_window.73e25c9f": "Current window:",
|
||||
"i18n:govoplan-admin.automatic.d9a7260f": "Automatic",
|
||||
"i18n:govoplan-admin.daemon_execution.cc0fad8d": "Daemon execution",
|
||||
"i18n:govoplan-admin.data_safety_reviewed.b4774edc": "Data safety reviewed",
|
||||
"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 role template",
|
||||
"i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d": "Delete {value0}? Removal is blocked while a materialized tenant definition still has members or assignments.",
|
||||
"i18n:govoplan-admin.delete_value.4d18989e": "Delete {value0}",
|
||||
"i18n:govoplan-admin.dependencies.9f4f78d1": "Dependencies:",
|
||||
"i18n:govoplan-admin.dependents.072665c4": "Dependents:",
|
||||
"i18n:govoplan-admin.description.55f8ebc8": "Description",
|
||||
"i18n:govoplan-admin.destructive.0051026c": "Destructive",
|
||||
"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 role template",
|
||||
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
|
||||
"i18n:govoplan-admin.enabled_modules.3c38e9ff": "Enabled modules:",
|
||||
"i18n:govoplan-admin.executor_available.507de429": "Executor available",
|
||||
"i18n:govoplan-admin.executor_pending.7633225b": "Executor pending",
|
||||
"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.forward_only.6c107a46": "Forward-only",
|
||||
"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.idempotent.47a0f2d6": "Idempotent",
|
||||
"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.manifest_metadata.b9ae362a": "Manifest metadata",
|
||||
"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.metadata_pending.25190d87": "Metadata pending",
|
||||
"i18n:govoplan-admin.migration_plan.f42b9d90": "Migration plan",
|
||||
"i18n:govoplan-admin.migration_safety.729bdb3c": "Migration safety:",
|
||||
"i18n:govoplan-admin.migration_tasks.a4410d3a": "Migration tasks",
|
||||
"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_idempotent.d3014c71": "Not idempotent",
|
||||
"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.pending_metadata.a24cfeb0": "Pending metadata",
|
||||
"i18n:govoplan-admin.permissions.d06d5557": "Permissions",
|
||||
"i18n:govoplan-admin.plan_install.e82bffe6": "Plan install",
|
||||
"i18n:govoplan-admin.plan_update.86e6857a": "Plan update",
|
||||
"i18n:govoplan-admin.post_migration_backfill.21043d74": "Post-migration backfill",
|
||||
"i18n:govoplan-admin.post_migration_verify.6af9f28b": "Post-migration verify",
|
||||
"i18n:govoplan-admin.pre_migration_check.f654164a": "Pre-migration check",
|
||||
"i18n:govoplan-admin.pre_migration_prepare.d18fdd26": "Pre-migration prepare",
|
||||
"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.provides.221b70d9": "Provides:",
|
||||
"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.recovery_tested.e831e2f1": "Recovery tested",
|
||||
"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.requires_review.8ba0b8c6": "Review required",
|
||||
"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.retirement.6b680dd2": "Retirement",
|
||||
"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 role template",
|
||||
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Save the install plan before queueing a daemon request.",
|
||||
"i18n:govoplan-admin.saved.c0ae8f6e": "Saved",
|
||||
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
|
||||
"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_plan.524ea0c8": "Target plan",
|
||||
"i18n:govoplan-admin.task_version.f0f26b92": "Task version",
|
||||
"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": "Permissions",
|
||||
"i18n:govoplan-admin.tenant_role.6b53115d": "Role template",
|
||||
"i18n:govoplan-admin.tenant_roles.51aca82d": "Role templates",
|
||||
"i18n:govoplan-admin.tenant_users.cb800b38": "Tenant users",
|
||||
"i18n:govoplan-admin.tenants.1f7ae776": "Tenants",
|
||||
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "These settings are enforced by the backend. Central groups and tenant roles remain available even when local creation is disabled.",
|
||||
"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.upgrade.12c5007d": "Upgrade",
|
||||
"i18n:govoplan-admin.update.503a059f": "Update",
|
||||
"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.administration_data_is_loading.6bf3c001": "Administrationsdaten werden geladen.",
|
||||
"i18n:govoplan-admin.an_administration_operation_is_in_progress.6bf3c002": "Eine Administrationsaktion wird gerade ausgeführt.",
|
||||
"i18n:govoplan-admin.system_administration_write_permission_is_required.6bf3c003": "Eine Schreibberechtigung für die Systemadministration ist erforderlich.",
|
||||
"i18n:govoplan-admin.system_governance_write_permission_is_required.6bf3c004": "Eine Schreibberechtigung für die System-Governance ist erforderlich.",
|
||||
"i18n:govoplan-admin.complete_the_required_fields_before_saving.6bf3c005": "Füllen Sie vor dem Speichern die Pflichtfelder aus.",
|
||||
"i18n:govoplan-admin.make_a_change_before_saving.6bf3c006": "Nehmen Sie vor dem Speichern eine Änderung vor.",
|
||||
"i18n:govoplan-admin.provide_valid_package_and_supplied_data_json.6bf3c007": "Geben Sie gültiges Paket- und Zusatzdaten-JSON ein.",
|
||||
"i18n:govoplan-admin.provide_valid_package_json_before_requesting_approval.6bf3c008": "Geben Sie vor der Genehmigungsanfrage gültiges Paket-JSON ein.",
|
||||
"i18n:govoplan-admin.add_at_least_one_valid_plan_item.6bf3c009": "Fügen Sie mindestens einen gültigen Planeintrag hinzu.",
|
||||
"i18n:govoplan-admin.save_the_changed_plan_before_queueing_it.6bf3c010": "Speichern Sie den geänderten Plan vor dem Einreihen.",
|
||||
"i18n:govoplan-admin.system_maintenance_authority_is_required.6bf3c011": "Die Berechtigung für die Systemwartung ist erforderlich.",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode_before_this_action.6bf3c012": "Aktivieren Sie vor dieser Aktion den Wartungsmodus.",
|
||||
"i18n:govoplan-admin.this_protected_module_cannot_be_changed.6bf3c013": "Dieses geschützte Modul kann nicht geändert werden.",
|
||||
"i18n:govoplan-admin.deactivate_the_module_before_planning_uninstall.6bf3c014": "Deaktivieren Sie das Modul, bevor Sie die Deinstallation planen.",
|
||||
"i18n:govoplan-admin.resolve_the_catalog_license_or_action_blocker_first.6bf3c015": "Beheben Sie zuerst die Katalog-, Lizenz- oder Paketaktionssperre.",
|
||||
"i18n:govoplan-admin.approve_configuration_change.6bf3c016": "Konfigurationsänderung genehmigen",
|
||||
"i18n:govoplan-admin.approving_records_your_authority_and_may_unlock_application.6bf3c017": "Die Genehmigung dokumentiert Ihre Autorität und kann die Anwendung dieser Konfigurationsänderung freigeben.",
|
||||
"i18n:govoplan-admin.apply_configuration_package.6bf3c018": "Konfigurationspaket anwenden",
|
||||
"i18n:govoplan-admin.apply_the_current_package_to_the_selected_scope.6bf3c019": "Das aktuelle Paket auf den ausgewählten Bereich anwenden? Dadurch kann modulbezogene Konfiguration erstellt oder geändert werden.",
|
||||
"i18n:govoplan-admin.apply_package.6bf3c020": "Paket anwenden",
|
||||
"i18n:govoplan-admin.enter_reference_ids_manually.6bf3c021": "Referenz-IDs manuell eingeben",
|
||||
"i18n:govoplan-admin.use_manual_ids_only_for_intentional_historical_references.6bf3c022": "Verwenden Sie manuelle IDs nur, wenn ein Paket absichtlich auf ein nicht mehr im Live-Katalog enthaltenes Ziel verweist.",
|
||||
"i18n:govoplan-admin.configuration_package_tenant.6bf3c023": "Mandant des Konfigurationspakets",
|
||||
"i18n:govoplan-admin.select_a_tenant.6bf3c024": "Mandanten auswählen",
|
||||
"i18n:govoplan-admin.configuration_package_change_request.6bf3c025": "Änderungsantrag für das Konfigurationspaket",
|
||||
"i18n:govoplan-admin.select_an_eligible_request_optional.6bf3c026": "Geeigneten Antrag auswählen (optional)",
|
||||
"i18n:govoplan-admin.no_eligible_configuration_requests.6bf3c027": "Keine geeigneten Konfigurationsanträge vorhanden.",
|
||||
"i18n:govoplan-admin.global.6bf3c028": "Global",
|
||||
"i18n:govoplan-admin.tenant.6bf3c029": "Mandant",
|
||||
"i18n:govoplan-admin.user.6bf3c030": "Benutzer",
|
||||
"i18n:govoplan-admin.request_is_not_pending_approval.6bf3c031": "Dieser Antrag wartet nicht auf eine Genehmigung.",
|
||||
"i18n:govoplan-admin.clear_saved_module_plan.6bf3c032": "Gespeicherten Modulplan leeren",
|
||||
"i18n:govoplan-admin.clear_all_saved_module_install_update_and_uninstall_items.6bf3c033": "Alle gespeicherten Installations-, Aktualisierungs- und Deinstallationseinträge löschen?",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode.6bf3c034": "Wartungsmodus aktivieren",
|
||||
"i18n:govoplan-admin.restrict_normal_authenticated_access_while_module_work_runs.6bf3c035": "Den normalen authentifizierten Zugriff während der beaufsichtigten Modularbeiten einschränken?",
|
||||
"i18n:govoplan-admin.cancel_installer_request.6bf3c036": "Installationsantrag abbrechen",
|
||||
"i18n:govoplan-admin.cancel_the_queued_request_before_the_daemon_accepts_it.6bf3c037": "Diesen eingereihten Antrag abbrechen, bevor der Installationsdienst ihn annimmt?",
|
||||
"i18n:govoplan-admin.this_option_only_applies_to_uninstall_plans.6bf3c038": "Diese Option gilt nur für Deinstallationspläne.",
|
||||
"i18n:govoplan-admin.this_option_only_applies_to_install_and_update_plans.6bf3c039": "Diese Option gilt nur für Installations- und Aktualisierungspläne.",
|
||||
"i18n:govoplan-admin.module_lifecycle_progress.f1a20201": "Fortschritt des Modullebenszyklus",
|
||||
"i18n:govoplan-admin.queue_supervised_run_unavailable.f1a20202": "Ein überwachter Lauf kann nicht eingereiht werden",
|
||||
"i18n:govoplan-admin.required_action.f1a20203": "Erforderliche Maßnahme",
|
||||
"i18n:govoplan-admin.who_can_fix_it.f1a20204": "Zuständig",
|
||||
"i18n:govoplan-admin.where_to_go.f1a20205": "Ziel",
|
||||
"i18n:govoplan-admin.current_operator.f1a20206": "Aktuell ausführende Person",
|
||||
"i18n:govoplan-admin.system_administrator.f1a20207": "Systemadministration",
|
||||
"i18n:govoplan-admin.module_write_access_is_required.f1a20208": "Schreibzugriff auf Module ist erforderlich.",
|
||||
"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.acknowledged.d08d7c6d": "Bestaetigt",
|
||||
"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.after.695c89be": "Nach:",
|
||||
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
|
||||
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
|
||||
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Rollenvorlage hinzufügen",
|
||||
"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.before.a11cf31f": "Vor:",
|
||||
"i18n:govoplan-admin.bridge_release.176cf241": "Brueckenrelease",
|
||||
"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.catalog_metadata.9b96c41a": "Katalogmetadaten",
|
||||
"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.manual.17a486ca": "Manuell",
|
||||
"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": "Rollenvorlage erstellen",
|
||||
"i18n:govoplan-admin.created.0c78dab1": "Created:",
|
||||
"i18n:govoplan-admin.created.accf40c8": "Erstellt",
|
||||
"i18n:govoplan-admin.current_window.73e25c9f": "Aktuelles Fenster:",
|
||||
"i18n:govoplan-admin.automatic.d9a7260f": "Automatisch",
|
||||
"i18n:govoplan-admin.daemon_execution.cc0fad8d": "Daemon-Ausführung",
|
||||
"i18n:govoplan-admin.data_safety_reviewed.b4774edc": "Datensicherheit geprueft",
|
||||
"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": "Rollenvorlage löschen",
|
||||
"i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d": "Delete {value0}? Removal is blocked while a materialized tenant definition still has members or assignments.",
|
||||
"i18n:govoplan-admin.delete_value.4d18989e": "Delete {value0}",
|
||||
"i18n:govoplan-admin.dependencies.9f4f78d1": "Abhaengigkeiten:",
|
||||
"i18n:govoplan-admin.dependents.072665c4": "Dependents:",
|
||||
"i18n:govoplan-admin.description.55f8ebc8": "Beschreibung",
|
||||
"i18n:govoplan-admin.destructive.0051026c": "Destruktiv",
|
||||
"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": "Rollenvorlage bearbeiten",
|
||||
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
|
||||
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
|
||||
"i18n:govoplan-admin.enabled_modules.3c38e9ff": "Aktive Module:",
|
||||
"i18n:govoplan-admin.executor_available.507de429": "Ausfuehrer verfuegbar",
|
||||
"i18n:govoplan-admin.executor_pending.7633225b": "Ausfuehrer ausstehend",
|
||||
"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.forward_only.6c107a46": "Nur vorwaerts",
|
||||
"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.idempotent.47a0f2d6": "Idempotent",
|
||||
"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.manifest_metadata.b9ae362a": "Manifestmetadaten",
|
||||
"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.metadata_pending.25190d87": "Metadaten ausstehend",
|
||||
"i18n:govoplan-admin.migration_plan.f42b9d90": "Migrationsplan",
|
||||
"i18n:govoplan-admin.migration_safety.729bdb3c": "Migrationssicherheit:",
|
||||
"i18n:govoplan-admin.migration_tasks.a4410d3a": "Migrationsaufgaben",
|
||||
"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_idempotent.d3014c71": "Nicht idempotent",
|
||||
"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.pending_metadata.a24cfeb0": "Ausstehende Metadaten",
|
||||
"i18n:govoplan-admin.permissions.d06d5557": "Berechtigungen",
|
||||
"i18n:govoplan-admin.plan_install.e82bffe6": "Plan install",
|
||||
"i18n:govoplan-admin.plan_update.86e6857a": "Aktualisierung planen",
|
||||
"i18n:govoplan-admin.post_migration_backfill.21043d74": "Nach-Migration Backfill",
|
||||
"i18n:govoplan-admin.post_migration_verify.6af9f28b": "Nach-Migration Pruefung",
|
||||
"i18n:govoplan-admin.pre_migration_check.f654164a": "Vor-Migration Check",
|
||||
"i18n:govoplan-admin.pre_migration_prepare.d18fdd26": "Vor-Migration Vorbereitung",
|
||||
"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.provides.221b70d9": "Provides:",
|
||||
"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.recovery_tested.e831e2f1": "Recovery getestet",
|
||||
"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.requires_review.8ba0b8c6": "Pruefung erforderlich",
|
||||
"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.retirement.6b680dd2": "Stilllegung",
|
||||
"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": "Rollenvorlage speichern",
|
||||
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Speichern Sie den Installationsplan, bevor eine Daemon-Anfrage eingereiht wird.",
|
||||
"i18n:govoplan-admin.saved.c0ae8f6e": "Gespeichert",
|
||||
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
|
||||
"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_plan.524ea0c8": "Zielplan",
|
||||
"i18n:govoplan-admin.task_version.f0f26b92": "Aufgabenversion",
|
||||
"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": "Berechtigungen",
|
||||
"i18n:govoplan-admin.tenant_role.6b53115d": "Rollenvorlage",
|
||||
"i18n:govoplan-admin.tenant_roles.51aca82d": "Rollenvorlagen",
|
||||
"i18n:govoplan-admin.tenant_users.cb800b38": "Mandantenbenutzer",
|
||||
"i18n:govoplan-admin.tenants.1f7ae776": "Mandanten",
|
||||
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "Diese Einstellungen werden vom Backend erzwungen. Zentrale Gruppen und Mandantenrollen bleiben verfügbar, auch wenn lokale Erstellung deaktiviert ist.",
|
||||
"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.upgrade.12c5007d": "Upgrade",
|
||||
"i18n:govoplan-admin.update.503a059f": "Aktualisieren",
|
||||
"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,13 @@ export { default } from "./module";
|
||||
export * from "./module";
|
||||
export * from "./api/admin";
|
||||
export { default as AdminOverviewPanel } from "./features/admin/AdminOverviewPanel";
|
||||
export { default as ConfigurationPackagesPanel } from "./features/admin/ConfigurationPackagesPanel";
|
||||
export {
|
||||
configurationReferenceSelectors,
|
||||
createChangeRequestReferenceProvider,
|
||||
createTenantReferenceProvider
|
||||
} from "./features/admin/configurationReferenceProviders";
|
||||
export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel";
|
||||
export { default as TenantModuleManagementPanel } from "./features/admin/TenantModuleManagementPanel";
|
||||
export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel";
|
||||
export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui";
|
||||
|
||||
+163
-66
@@ -1,86 +1,183 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { AdminSectionsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { adminReadScopes, hasScope } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import { configurationReferenceSelectors } from "./features/admin/configurationReferenceProviders";
|
||||
|
||||
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 ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
|
||||
const TenantModuleManagementPanel = lazy(() => import("./features/admin/TenantModuleManagementPanel"));
|
||||
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
|
||||
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
de: generatedTranslations.de
|
||||
};
|
||||
|
||||
const adminSections: AdminSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "overview",
|
||||
label: "Overview",
|
||||
group: "ROOT",
|
||||
order: 0,
|
||||
anyOf: adminReadScopes,
|
||||
render: ({ settings, availableSections, selectSection }) => createElement(AdminOverviewPanel, {
|
||||
settings,
|
||||
availableSections,
|
||||
onSelect: selectSection
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-settings",
|
||||
label: "General",
|
||||
group: "SYSTEM",
|
||||
order: 10,
|
||||
allOf: ["system:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(SystemSettingsPanel, {
|
||||
settings,
|
||||
canWrite: hasScope(auth, "system:settings:write"),
|
||||
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-role-templates",
|
||||
label: "Tenant roles",
|
||||
group: "SYSTEM",
|
||||
order: 40,
|
||||
allOf: ["system:governance:read"],
|
||||
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
|
||||
settings,
|
||||
kind: "role",
|
||||
canWrite: hasScope(auth, "system:governance:write"),
|
||||
onAuthRefresh: refreshAuth
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-modules",
|
||||
label: "Modules",
|
||||
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: "Groups",
|
||||
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
|
||||
})
|
||||
}
|
||||
]
|
||||
{
|
||||
id: "overview",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.overview",
|
||||
label: "i18n:govoplan-admin.overview.0efc2e6b",
|
||||
group: "ROOT",
|
||||
order: 0,
|
||||
anyOf: adminReadScopes,
|
||||
render: ({ settings, availableSections, selectSection }) => createElement(AdminOverviewPanel, {
|
||||
settings,
|
||||
availableSections,
|
||||
onSelect: selectSection
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-settings",
|
||||
moduleId: "admin",
|
||||
kind: "settings",
|
||||
surfaceId: "admin.section.system-settings",
|
||||
label: "i18n:govoplan-admin.general.9239ee2c",
|
||||
group: "SYSTEM",
|
||||
order: 10,
|
||||
allOf: ["system:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(SystemSettingsPanel, {
|
||||
settings,
|
||||
canWrite: hasScope(auth, "system:settings:write"),
|
||||
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-configuration-changes",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.system-configuration-changes",
|
||||
label: "i18n:govoplan-admin.changes.8aa57de6",
|
||||
group: "SYSTEM",
|
||||
order: 20,
|
||||
allOf: ["system:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(ConfigurationChangesPanel, {
|
||||
settings,
|
||||
canApprove: hasScope(auth, "system:settings:write") || hasScope(auth, "system:governance:write")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-configuration-packages",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.system-configuration-packages",
|
||||
label: "i18n:govoplan-admin.configuration_packages.eb2f05f1",
|
||||
group: "SYSTEM",
|
||||
order: 30,
|
||||
allOf: ["system:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(ConfigurationPackagesPanel, {
|
||||
settings,
|
||||
auth,
|
||||
canWrite: hasScope(auth, "system:settings:write")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-role-templates",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.system-role-templates",
|
||||
label: "i18n:govoplan-admin.tenant_roles.51aca82d",
|
||||
group: "SYSTEM",
|
||||
order: 40,
|
||||
allOf: ["system:governance:read"],
|
||||
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
|
||||
settings,
|
||||
kind: "role",
|
||||
canWrite: hasScope(auth, "system:governance:write"),
|
||||
onAuthRefresh: refreshAuth
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-modules",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.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-tenant-modules",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.system-tenant-modules",
|
||||
label: "Tenant modules",
|
||||
group: "SYSTEM",
|
||||
order: 86,
|
||||
allOf: ["system:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(TenantModuleManagementPanel, {
|
||||
settings,
|
||||
scope: "system",
|
||||
canWrite: hasScope(auth, "system:settings:write")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "tenant-modules",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.tenant-modules",
|
||||
label: "Modules",
|
||||
group: "TENANT",
|
||||
order: 60,
|
||||
anyOf: ["admin:module:read", "admin:module:write", "system:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(TenantModuleManagementPanel, {
|
||||
settings,
|
||||
scope: "tenant",
|
||||
canWrite: hasScope(auth, "admin:module:write") || hasScope(auth, "system:settings:write")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "system-groups",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.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 = {
|
||||
id: "admin",
|
||||
label: "Admin",
|
||||
version: "1.0.0",
|
||||
label: "i18n:govoplan-admin.admin.4e7afebc",
|
||||
version: "0.1.8",
|
||||
dependencies: ["access"],
|
||||
translations,
|
||||
viewSurfaces: [
|
||||
{ id: "admin.section.overview", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.overview.0efc2e6b", order: 0 },
|
||||
{ id: "admin.section.system-settings", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.system_general_settings.7cd662ed", order: 10 },
|
||||
{ id: "admin.section.system-configuration-changes", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_changes.82933bbb", order: 20 },
|
||||
{ id: "admin.section.system-configuration-packages", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_packages.eb2f05f1", order: 30 },
|
||||
{ id: "admin.section.system-role-templates", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.tenant_roles.51aca82d", order: 40 },
|
||||
{ id: "admin.section.system-groups", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.central_groups.5c9b5b66", order: 50 },
|
||||
{ id: "admin.section.system-modules", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.modules.04e9462c", order: 85 },
|
||||
{ id: "admin.section.system-tenant-modules", moduleId: "admin", kind: "section", label: "Tenant modules", order: 86 },
|
||||
{ id: "admin.section.tenant-modules", moduleId: "admin", kind: "section", label: "Modules", order: 60 }
|
||||
],
|
||||
uiCapabilities: {
|
||||
"admin.sections": adminSections
|
||||
"admin.sections": adminSections,
|
||||
"admin.configurationReferences": configurationReferenceSelectors
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
installerRequestMatchesPlan,
|
||||
moduleInstallerQueueBlock,
|
||||
moduleInstallerWorkflowStages,
|
||||
type ModuleInstallerWorkflowInput
|
||||
} from "../src/features/admin/moduleInstallerWorkflow.ts";
|
||||
|
||||
const ready: ModuleInstallerWorkflowInput = {
|
||||
planItemCount: 1,
|
||||
planDirty: false,
|
||||
planValid: true,
|
||||
preflightAllowed: true,
|
||||
maintenanceEnabled: true,
|
||||
canWrite: true,
|
||||
canAccessMaintenance: true
|
||||
};
|
||||
|
||||
test("keeps the operator on the earliest incomplete installer stage", () => {
|
||||
assert.equal(moduleInstallerWorkflowStages({ ...ready, planItemCount: 0 })[0].current, true);
|
||||
assert.equal(moduleInstallerWorkflowStages({ ...ready, planDirty: true })[0].current, true);
|
||||
assert.equal(moduleInstallerWorkflowStages({ ...ready, preflightAllowed: false })[1].state, "blocked");
|
||||
assert.equal(moduleInstallerWorkflowStages({ ...ready, maintenanceEnabled: false })[2].state, "blocked");
|
||||
assert.equal(moduleInstallerWorkflowStages(ready)[2].state, "current");
|
||||
});
|
||||
|
||||
test("moves queued work through execution to durable evidence", () => {
|
||||
const queued = moduleInstallerWorkflowStages({ ...ready, requestStatus: "queued" });
|
||||
assert.equal(queued[2].state, "complete");
|
||||
assert.equal(queued[3].state, "current");
|
||||
|
||||
const completed = moduleInstallerWorkflowStages({
|
||||
...ready,
|
||||
requestStatus: "completed",
|
||||
runStatus: "completed"
|
||||
});
|
||||
assert.deepEqual(completed.map((stage) => stage.state), [
|
||||
"complete",
|
||||
"complete",
|
||||
"complete",
|
||||
"complete",
|
||||
"current"
|
||||
]);
|
||||
|
||||
const failed = moduleInstallerWorkflowStages({
|
||||
...ready,
|
||||
requestStatus: "failed",
|
||||
runStatus: "rollback_failed"
|
||||
});
|
||||
assert.equal(failed[4].state, "failed");
|
||||
});
|
||||
|
||||
test("reports one actionable queue blocker in deterministic order", () => {
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, canWrite: false }), "write_access");
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, planItemCount: 0 }), "empty_plan");
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, planValid: false }), "invalid_plan");
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, planDirty: true }), "unsaved_plan");
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, preflightAllowed: false }), "preflight");
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, maintenanceEnabled: false }), "maintenance_mode");
|
||||
assert.equal(moduleInstallerQueueBlock({ ...ready, canAccessMaintenance: false }), "maintenance_access");
|
||||
assert.equal(
|
||||
moduleInstallerQueueBlock({ ...ready, maintenanceEnabled: false, canAccessMaintenance: false }),
|
||||
"maintenance_access"
|
||||
);
|
||||
assert.equal(moduleInstallerQueueBlock(ready), null);
|
||||
});
|
||||
|
||||
test("does not present an older installer request as evidence for a newer plan", () => {
|
||||
assert.equal(installerRequestMatchesPlan(null, "2026-08-03T10:00:00Z"), true);
|
||||
assert.equal(
|
||||
installerRequestMatchesPlan("2026-08-03T10:00:00Z", "2026-08-03T10:00:01Z"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
installerRequestMatchesPlan("2026-08-03T10:00:00Z", "2026-08-03T09:59:59Z"),
|
||||
false
|
||||
);
|
||||
assert.equal(installerRequestMatchesPlan("invalid", "2026-08-03T10:00:00Z"), false);
|
||||
});
|
||||
Reference in New Issue
Block a user