Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2baa8f2657 | ||
|
|
6176e9f40e | ||
|
|
11b9b7c4c6 |
@@ -85,6 +85,14 @@ storage, or reinterpret an infrastructure replacement as safe. Use the Files
|
|||||||
integrity and Ops checks after deployment and complete migration/recovery review
|
integrity and Ops checks after deployment and complete migration/recovery review
|
||||||
before changing an active backend.
|
before changing an active backend.
|
||||||
|
|
||||||
|
The Files infrastructure dependency provider makes that review concrete. Its
|
||||||
|
authorized, non-secret Ops inventory reports the active runtime binding and
|
||||||
|
aggregates persisted `FileBlob` rows by storage backend with blob counts and
|
||||||
|
byte totals. A host apply that changes `files.storage` requires a fresh,
|
||||||
|
complete inventory from the same installation and shows the migration and
|
||||||
|
checksum-verification action before any service is replaced. Object keys,
|
||||||
|
tenant identifiers and storage credentials are not exported.
|
||||||
|
|
||||||
## User tasks
|
## User tasks
|
||||||
|
|
||||||
The Files page is available at `/files`. Actions appear only when the current
|
The Files page is available at `/files`. Actions appear only when the current
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/files-webui",
|
"name": "@govoplan/files-webui",
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"react-dom": ">=19.2.7 <20",
|
"react-dom": ">=19.2.7 <20",
|
||||||
"react-router": ">=8.3.0 <9",
|
"react-router": ">=8.3.0 <9",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"@govoplan/core-webui": "^0.1.18"
|
"@govoplan/core-webui": "^0.1.44"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@govoplan/core-webui": {
|
"@govoplan/core-webui": {
|
||||||
|
|||||||
+2
-2
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-files"
|
name = "govoplan-files"
|
||||||
version = "0.1.22"
|
version = "0.1.25"
|
||||||
description = "GovOPlaN files module with backend and WebUI integration."
|
description = "GovOPlaN files module with backend and WebUI integration."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.20",
|
"govoplan-core>=0.1.44",
|
||||||
"defusedxml>=0.7,<1",
|
"defusedxml>=0.7,<1",
|
||||||
"pyzipper>=0.3.6,<1",
|
"pyzipper>=0.3.6,<1",
|
||||||
"python-multipart>=0.0.31,<1",
|
"python-multipart>=0.0.31,<1",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from govoplan_core.core.configuration_packages import (
|
from govoplan_core.core.configuration_packages import (
|
||||||
ConfigurationApplyResult,
|
ConfigurationApplyResult,
|
||||||
ConfigurationDiagnostic,
|
ConfigurationDiagnostic,
|
||||||
@@ -21,19 +23,30 @@ from govoplan_core.core.configuration_packages import (
|
|||||||
from govoplan_core.core.infrastructure_capabilities import (
|
from govoplan_core.core.infrastructure_capabilities import (
|
||||||
InfrastructureCapability,
|
InfrastructureCapability,
|
||||||
InfrastructureCapabilityReceipt,
|
InfrastructureCapabilityReceipt,
|
||||||
|
InfrastructureDependency,
|
||||||
|
InfrastructureDependencyProvider,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.db.session import get_database
|
||||||
|
from govoplan_files.backend.db.models import FileBlob
|
||||||
from govoplan_files.backend.runtime import settings as runtime_settings
|
from govoplan_files.backend.runtime import settings as runtime_settings
|
||||||
|
|
||||||
|
|
||||||
FILES_CONFIGURATION_CAPABILITY = "files.configuration"
|
FILES_CONFIGURATION_CAPABILITY = "files.configuration"
|
||||||
|
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY = (
|
||||||
|
"infrastructure.dependency_inventory.files"
|
||||||
|
)
|
||||||
MANAGED_STORAGE_FRAGMENT = "managed_storage"
|
MANAGED_STORAGE_FRAGMENT = "managed_storage"
|
||||||
_PAYLOAD_KEYS = frozenset(
|
_PAYLOAD_KEYS = frozenset(
|
||||||
{"capability_id", "expected_backend", "expected_source"}
|
{"capability_id", "expected_backend", "expected_source"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FilesConfigurationProvider(ConfigurationProvider):
|
class FilesConfigurationProvider(
|
||||||
|
ConfigurationProvider,
|
||||||
|
InfrastructureDependencyProvider,
|
||||||
|
):
|
||||||
module_id = "files"
|
module_id = "files"
|
||||||
|
capability_ids = ("files.storage",)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -166,6 +179,61 @@ class FilesConfigurationProvider(ConfigurationProvider):
|
|||||||
item for item in import_result.diagnostics if item.severity == "blocker"
|
item for item in import_result.diagnostics if item.severity == "blocker"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
|
||||||
|
active_backend = _normalized_backend(
|
||||||
|
getattr(self._settings, "file_storage_backend", "local")
|
||||||
|
)
|
||||||
|
dependencies = [
|
||||||
|
InfrastructureDependency(
|
||||||
|
capability_id="files.storage",
|
||||||
|
module_id="files",
|
||||||
|
dependency_type="runtime_storage_binding",
|
||||||
|
dependency_ref=f"files-storage:{active_backend}",
|
||||||
|
state="runtime_binding",
|
||||||
|
scope="system",
|
||||||
|
summary=(
|
||||||
|
"The active Files runtime is bound to this deployment storage backend."
|
||||||
|
),
|
||||||
|
metrics={},
|
||||||
|
required_action=(
|
||||||
|
"Provision and verify the replacement backend before rebinding the Files runtime."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
with get_database().session() as session:
|
||||||
|
rows = session.execute(
|
||||||
|
select(
|
||||||
|
FileBlob.storage_backend,
|
||||||
|
func.count(FileBlob.id),
|
||||||
|
func.coalesce(func.sum(FileBlob.size_bytes), 0),
|
||||||
|
)
|
||||||
|
.group_by(FileBlob.storage_backend)
|
||||||
|
.order_by(FileBlob.storage_backend)
|
||||||
|
)
|
||||||
|
for backend, blob_count, size_bytes in rows:
|
||||||
|
normalized_backend = _normalized_backend(str(backend or "local"))
|
||||||
|
dependencies.append(
|
||||||
|
InfrastructureDependency(
|
||||||
|
capability_id="files.storage",
|
||||||
|
module_id="files",
|
||||||
|
dependency_type="stored_blob_set",
|
||||||
|
dependency_ref=f"file-blobs:{normalized_backend}",
|
||||||
|
state="data_present",
|
||||||
|
scope="all-tenants",
|
||||||
|
summary=(
|
||||||
|
"Persisted Files blob metadata references content in this storage backend."
|
||||||
|
),
|
||||||
|
metrics={
|
||||||
|
"blob_count": int(blob_count or 0),
|
||||||
|
"content_bytes": int(size_bytes or 0),
|
||||||
|
},
|
||||||
|
required_action=(
|
||||||
|
"Copy and checksum-verify every referenced blob, switch the runtime binding, and retain rollback evidence before removing or replacing storage."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(dependencies)
|
||||||
|
|
||||||
def _binding_diagnostics(
|
def _binding_diagnostics(
|
||||||
self,
|
self,
|
||||||
fragment: ConfigurationPackageFragment,
|
fragment: ConfigurationPackageFragment,
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ from govoplan_core.core.modules import (
|
|||||||
ModuleManifest,
|
ModuleManifest,
|
||||||
NavItem,
|
NavItem,
|
||||||
PermissionDefinition,
|
PermissionDefinition,
|
||||||
|
ProductAvailabilityExplanation,
|
||||||
ProductAreaContribution,
|
ProductAreaContribution,
|
||||||
|
ProductSurfaceContribution,
|
||||||
QuickAccessTool,
|
QuickAccessTool,
|
||||||
RoleTemplate,
|
RoleTemplate,
|
||||||
)
|
)
|
||||||
@@ -55,6 +57,7 @@ from govoplan_core.db.base import Base
|
|||||||
from govoplan_files.backend.change_tracking import register_files_change_tracking
|
from govoplan_files.backend.change_tracking import register_files_change_tracking
|
||||||
from govoplan_files.backend.configuration_provider import (
|
from govoplan_files.backend.configuration_provider import (
|
||||||
FILES_CONFIGURATION_CAPABILITY,
|
FILES_CONFIGURATION_CAPABILITY,
|
||||||
|
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
|
||||||
)
|
)
|
||||||
from govoplan_files.backend.db import models as file_models # noqa: F401 - populate Files ORM metadata
|
from govoplan_files.backend.db import models as file_models # noqa: F401 - populate Files ORM metadata
|
||||||
from govoplan_files.backend.documentation import documentation_topics
|
from govoplan_files.backend.documentation import documentation_topics
|
||||||
@@ -458,7 +461,7 @@ def _dsar_provider(context: ModuleContext) -> object:
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="files",
|
id="files",
|
||||||
name="Files",
|
name="Files",
|
||||||
version="0.1.22",
|
version="0.1.25",
|
||||||
required_capabilities=(
|
required_capabilities=(
|
||||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||||
@@ -605,6 +608,31 @@ manifest = ModuleManifest(
|
|||||||
order=30,
|
order=30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
product_surfaces=(
|
||||||
|
ProductSurfaceContribution(
|
||||||
|
id="records.files",
|
||||||
|
module_id="files",
|
||||||
|
label="i18n:govoplan-core.product_surface.files",
|
||||||
|
description="i18n:govoplan-core.product_surface.files_description",
|
||||||
|
icon="folder",
|
||||||
|
entry_path="/documents",
|
||||||
|
route_path="/files",
|
||||||
|
surface_ids=("files.nav.files", "files.route.files"),
|
||||||
|
presentations=("task", "reader"),
|
||||||
|
search_source_ids=("files.objects",),
|
||||||
|
help_context_ids=("files.list",),
|
||||||
|
documentation_topic_ids=("files.quick-access-and-product-area",),
|
||||||
|
required_any=("files:file:read",),
|
||||||
|
order=10,
|
||||||
|
unavailable=ProductAvailabilityExplanation(
|
||||||
|
reason="authorization",
|
||||||
|
title="i18n:govoplan-core.product_surface.unavailable",
|
||||||
|
description="i18n:govoplan-core.product_surface.unavailable_description",
|
||||||
|
resolution="i18n:govoplan-core.product_surface.unavailable_resolution",
|
||||||
|
responsible_role="i18n:govoplan-core.access_administrator",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
quick_access_tools=(
|
quick_access_tools=(
|
||||||
QuickAccessTool(
|
QuickAccessTool(
|
||||||
id="files.recent",
|
id="files.recent",
|
||||||
@@ -720,7 +748,8 @@ manifest = ModuleManifest(
|
|||||||
"The Files configuration provider validates the files.storage capability against the effective local or S3 runtime. "
|
"The Files configuration provider validates the files.storage capability against the effective local or S3 runtime. "
|
||||||
"It checks the backend, sanitized endpoint, bucket, trust or management marker, durable local path, and presence of referenced environment secrets. "
|
"It checks the backend, sanitized endpoint, bucket, trust or management marker, durable local path, and presence of referenced environment secrets. "
|
||||||
"Matching configuration is already effective and therefore reports skip on every apply. Drift blocks the package; Files never copies secret values, rewrites process environment, "
|
"Matching configuration is already effective and therefore reports skip on every apply. Drift blocks the package; Files never copies secret values, rewrites process environment, "
|
||||||
"or treats a storage replacement as an implicit migration. Run Files integrity and Ops checks after deployment changes."
|
"or treats a storage replacement as an implicit migration. Before files.storage changes, Files reports the active runtime binding plus persisted blob counts and byte totals per backend "
|
||||||
|
"through the non-secret Ops dependency inventory. Missing, stale, or incomplete inventory blocks host apply. Run Files integrity and Ops checks before and after deployment changes."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("admin",),
|
documentation_types=("admin",),
|
||||||
@@ -752,7 +781,8 @@ manifest = ModuleManifest(
|
|||||||
"Der Files-Konfigurationsprovider prüft die Fähigkeit files.storage gegen die wirksame lokale oder S3-Laufzeitkonfiguration. "
|
"Der Files-Konfigurationsprovider prüft die Fähigkeit files.storage gegen die wirksame lokale oder S3-Laufzeitkonfiguration. "
|
||||||
"Geprüft werden Backend, bereinigter Endpunkt, Bucket, Vertrauens- oder Verwaltungskennzeichen, dauerhafter lokaler Pfad sowie das Vorhandensein referenzierter Umgebungsgeheimnisse. "
|
"Geprüft werden Backend, bereinigter Endpunkt, Bucket, Vertrauens- oder Verwaltungskennzeichen, dauerhafter lokaler Pfad sowie das Vorhandensein referenzierter Umgebungsgeheimnisse. "
|
||||||
"Eine passende Konfiguration ist bereits wirksam und meldet deshalb bei jeder Anwendung skip. Abweichungen blockieren das Paket; Files kopiert keine Geheimwerte, verändert keine Prozessumgebung "
|
"Eine passende Konfiguration ist bereits wirksam und meldet deshalb bei jeder Anwendung skip. Abweichungen blockieren das Paket; Files kopiert keine Geheimwerte, verändert keine Prozessumgebung "
|
||||||
"und behandelt einen Speicherwechsel nicht als stillschweigende Migration. Nach Bereitstellungsänderungen sind die Integritäts- und Ops-Prüfungen auszuführen."
|
"und behandelt einen Speicherwechsel nicht als stillschweigende Migration. Vor einer Änderung von files.storage meldet Files die aktive Laufzeitbindung sowie gespeicherte Blob-Anzahlen und Byte-Summen je Backend "
|
||||||
|
"im nicht geheimen Ops-Abhängigkeitsinventar. Ein fehlendes, veraltetes oder unvollständiges Inventar blockiert die Host-Anwendung. Die Integritäts- und Ops-Prüfungen sind vor und nach der Bereitstellungsänderung auszuführen."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -768,7 +798,8 @@ manifest = ModuleManifest(
|
|||||||
title="Files in Records and documents and Quick Access",
|
title="Files in Records and documents and Quick Access",
|
||||||
summary="Use managed files in the Records and documents area and keep a compact file surface beside current work.",
|
summary="Use managed files in the Records and documents area and keep a compact file surface beside current work.",
|
||||||
body=(
|
body=(
|
||||||
"Files contributes its authorized workspace to Records and documents. When Quick Access is enabled, the owner-rendered "
|
"Files contributes its authorized workspace to the stable Files destination at /documents in Records and documents. "
|
||||||
|
"The owner route /files remains available through All available tools and as a compatible deep link. When Quick Access is enabled, the owner-rendered "
|
||||||
"selector shows at most seven recently changed files and can return one exact file-version reference to the current task. "
|
"selector shows at most seven recently changed files and can return one exact file-version reference to the current task. "
|
||||||
"Accounts with upload permission can launch the full Files upload dialog into a freshly reauthorized managed space. Opening "
|
"Accounts with upload permission can launch the full Files upload dialog into a freshly reauthorized managed space. Opening "
|
||||||
"either path causes Files to re-run its own provider, space, folder, object, and scope checks; completion and cancellation are "
|
"either path causes Files to re-run its own provider, space, folder, object, and scope checks; completion and cancellation are "
|
||||||
@@ -798,7 +829,8 @@ manifest = ModuleManifest(
|
|||||||
"title": "Dateien in Akten und Dokumente sowie im Schnellzugriff",
|
"title": "Dateien in Akten und Dokumente sowie im Schnellzugriff",
|
||||||
"summary": "Verwaltete Dateien im Produktbereich Akten und Dokumente und optional neben der aktuellen Arbeit verwenden.",
|
"summary": "Verwaltete Dateien im Produktbereich Akten und Dokumente und optional neben der aktuellen Arbeit verwenden.",
|
||||||
"body": (
|
"body": (
|
||||||
"Files ordnet seinen berechtigten Arbeitsbereich Akten und Dokumente zu. Ist der Schnellzugriff aktiviert, erscheint "
|
"Files ordnet seinen berechtigten Arbeitsbereich dem stabilen Produktziel Dateien unter /documents in Akten und Dokumente zu. "
|
||||||
|
"Der Eigentümerpfad /files bleibt unter Alle verfügbaren Werkzeuge und als kompatibler Direktlink erreichbar. Ist der Schnellzugriff aktiviert, erscheint "
|
||||||
"die vom Modul gerenderte Auswahl von höchstens sieben zuletzt geänderten berechtigten Dateien rechts neben der aktuellen "
|
"die vom Modul gerenderte Auswahl von höchstens sieben zuletzt geänderten berechtigten Dateien rechts neben der aktuellen "
|
||||||
"Seite und kann einen exakten Dateiversionsverweis zurückgeben. Mit Upload-Berechtigung lässt sich der vollständige "
|
"Seite und kann einen exakten Dateiversionsverweis zurückgeben. Mit Upload-Berechtigung lässt sich der vollständige "
|
||||||
"Upload-Dialog in einem erneut berechtigungsgeprüften verwalteten Bereich öffnen. Files prüft Anbieter, Bereich, Ordner, "
|
"Upload-Dialog in einem erneut berechtigungsgeprüften verwalteten Bereich öffnen. Files prüft Anbieter, Bereich, Ordner, "
|
||||||
@@ -1784,6 +1816,7 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
capability_factories={
|
capability_factories={
|
||||||
FILES_CONFIGURATION_CAPABILITY: _configuration_provider,
|
FILES_CONFIGURATION_CAPABILITY: _configuration_provider,
|
||||||
|
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY: _configuration_provider,
|
||||||
CAPABILITY_FILES_ACCESS: lambda context: __import__(
|
CAPABILITY_FILES_ACCESS: lambda context: __import__(
|
||||||
"govoplan_files.backend.capabilities", fromlist=["access_capability"]
|
"govoplan_files.backend.capabilities", fromlist=["access_capability"]
|
||||||
).access_capability(context),
|
).access_capability(context),
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from govoplan_core.core.configuration_packages import (
|
from govoplan_core.core.configuration_packages import (
|
||||||
ConfigurationPackageFragment,
|
ConfigurationPackageFragment,
|
||||||
ConfigurationPreflightContext,
|
ConfigurationPreflightContext,
|
||||||
@@ -10,10 +14,14 @@ from govoplan_core.core.configuration_packages import (
|
|||||||
from govoplan_core.core.infrastructure_capabilities import (
|
from govoplan_core.core.infrastructure_capabilities import (
|
||||||
infrastructure_capability_receipt_from_mapping,
|
infrastructure_capability_receipt_from_mapping,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.db.base import Base
|
||||||
|
from govoplan_core.db.session import configure_database, reset_database
|
||||||
from govoplan_files.backend.configuration_provider import (
|
from govoplan_files.backend.configuration_provider import (
|
||||||
FILES_CONFIGURATION_CAPABILITY,
|
FILES_CONFIGURATION_CAPABILITY,
|
||||||
|
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
|
||||||
FilesConfigurationProvider,
|
FilesConfigurationProvider,
|
||||||
)
|
)
|
||||||
|
from govoplan_files.backend.db.models import FileBlob
|
||||||
from govoplan_files.backend.manifest import manifest
|
from govoplan_files.backend.manifest import manifest
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +86,67 @@ def _s3_settings(**overrides):
|
|||||||
class FilesConfigurationProviderTests(unittest.TestCase):
|
class FilesConfigurationProviderTests(unittest.TestCase):
|
||||||
def test_provider_is_registered(self) -> None:
|
def test_provider_is_registered(self) -> None:
|
||||||
self.assertIn(FILES_CONFIGURATION_CAPABILITY, manifest.capability_factories)
|
self.assertIn(FILES_CONFIGURATION_CAPABILITY, manifest.capability_factories)
|
||||||
|
self.assertIn(
|
||||||
|
FILES_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
|
||||||
|
manifest.capability_factories,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_inventory_reports_runtime_binding_and_persisted_blob_aggregate(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-files-inventory-") as root:
|
||||||
|
database_path = Path(root) / "files.sqlite3"
|
||||||
|
engine = create_engine(f"sqlite:///{database_path}")
|
||||||
|
Base.metadata.create_all(engine, tables=(FileBlob.__table__,))
|
||||||
|
configure_database(
|
||||||
|
f"sqlite:///{database_path}",
|
||||||
|
engine=engine,
|
||||||
|
dispose_previous=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(
|
||||||
|
FileBlob.__table__.insert(),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "blob-1",
|
||||||
|
"tenant_id": "tenant-1",
|
||||||
|
"storage_backend": "local",
|
||||||
|
"storage_key": "tenant-1/a",
|
||||||
|
"checksum_sha256": "a" * 64,
|
||||||
|
"size_bytes": 7,
|
||||||
|
"protection_discriminator": "plaintext",
|
||||||
|
"ref_count": 1,
|
||||||
|
"integrity_status": "unchecked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blob-2",
|
||||||
|
"tenant_id": "tenant-1",
|
||||||
|
"storage_backend": "local",
|
||||||
|
"storage_key": "tenant-1/b",
|
||||||
|
"checksum_sha256": "b" * 64,
|
||||||
|
"size_bytes": 11,
|
||||||
|
"protection_discriminator": "plaintext",
|
||||||
|
"ref_count": 1,
|
||||||
|
"integrity_status": "unchecked",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
provider = FilesConfigurationProvider(
|
||||||
|
settings=_local_settings(),
|
||||||
|
environment={},
|
||||||
|
)
|
||||||
|
|
||||||
|
dependencies = provider.infrastructure_dependencies()
|
||||||
|
finally:
|
||||||
|
reset_database()
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
["runtime_storage_binding", "stored_blob_set"],
|
||||||
|
[item.dependency_type for item in dependencies],
|
||||||
|
)
|
||||||
|
blob_set = dependencies[1]
|
||||||
|
self.assertEqual(2, blob_set.metrics["blob_count"])
|
||||||
|
self.assertEqual(18, blob_set.metrics["content_bytes"])
|
||||||
|
|
||||||
def test_matching_local_storage_is_an_idempotent_noop(self) -> None:
|
def test_matching_local_storage_is_an_idempotent_noop(self) -> None:
|
||||||
provider = FilesConfigurationProvider(
|
provider = FilesConfigurationProvider(
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/files-webui",
|
"name": "@govoplan/files-webui",
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"react-dom": ">=19.2.7 <20",
|
"react-dom": ">=19.2.7 <20",
|
||||||
"react-router": ">=8.3.0 <9",
|
"react-router": ">=8.3.0 <9",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"@govoplan/core-webui": "^0.1.18"
|
"@govoplan/core-webui": "^0.1.44"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@govoplan/core-webui": {
|
"@govoplan/core-webui": {
|
||||||
|
|||||||
@@ -1061,7 +1061,7 @@ export default function FileConnectorSettingsPanel({
|
|||||||
)}
|
)}
|
||||||
<div className="file-connector-settings-section file-connector-policy-locks">
|
<div className="file-connector-settings-section file-connector-policy-locks">
|
||||||
<ToggleSwitch label="i18n:govoplan-files.lower_connection_limits.4f9838bc" checked={policyDraft.allowLowerConnectionLimits} disabled={saving || !canWrite} onChange={(allowLowerConnectionLimits) => patchPolicyDraft({ allowLowerConnectionLimits })} />
|
<ToggleSwitch label="i18n:govoplan-files.lower_connection_limits.4f9838bc" checked={policyDraft.allowLowerConnectionLimits} disabled={saving || !canWrite} onChange={(allowLowerConnectionLimits) => patchPolicyDraft({ allowLowerConnectionLimits })} />
|
||||||
<ToggleSwitch label="i18n:govoplan-files.lower_credential_limits.ec2b72bc" checked={policyDraft.allowLowerCredentialLimits} disabled={saving || !canWrite} onChange={(allowLowerCredentialLimits) => patchPolicyDraft({ allowLowerCredentialLimits })} />
|
<ToggleSwitch label="i18n:govoplan-files.lower_credential_limits.ec2b72bc" helpContextId="files.connector.policy" helpModuleId="files" checked={policyDraft.allowLowerCredentialLimits} disabled={saving || !canWrite} onChange={(allowLowerCredentialLimits) => patchPolicyDraft({ allowLowerCredentialLimits })} />
|
||||||
<ToggleSwitch label="i18n:govoplan-files.lower_provider_limits.5816270a" checked={policyDraft.allowLowerProviderLimits} disabled={saving || !canWrite} onChange={(allowLowerProviderLimits) => patchPolicyDraft({ allowLowerProviderLimits })} />
|
<ToggleSwitch label="i18n:govoplan-files.lower_provider_limits.5816270a" checked={policyDraft.allowLowerProviderLimits} disabled={saving || !canWrite} onChange={(allowLowerProviderLimits) => patchPolicyDraft({ allowLowerProviderLimits })} />
|
||||||
<ToggleSwitch label="i18n:govoplan-files.lower_path_limits.1abcccb1" checked={policyDraft.allowLowerPathLimits} disabled={saving || !canWrite} onChange={(allowLowerPathLimits) => patchPolicyDraft({ allowLowerPathLimits })} />
|
<ToggleSwitch label="i18n:govoplan-files.lower_path_limits.1abcccb1" checked={policyDraft.allowLowerPathLimits} disabled={saving || !canWrite} onChange={(allowLowerPathLimits) => patchPolicyDraft({ allowLowerPathLimits })} />
|
||||||
<ToggleSwitch label="i18n:govoplan-files.lower_endpoint_limits.3fd781d5" checked={policyDraft.allowLowerUrlLimits} disabled={saving || !canWrite} onChange={(allowLowerUrlLimits) => patchPolicyDraft({ allowLowerUrlLimits })} />
|
<ToggleSwitch label="i18n:govoplan-files.lower_endpoint_limits.3fd781d5" checked={policyDraft.allowLowerUrlLimits} disabled={saving || !canWrite} onChange={(allowLowerUrlLimits) => patchPolicyDraft({ allowLowerUrlLimits })} />
|
||||||
@@ -1101,14 +1101,14 @@ export default function FileConnectorSettingsPanel({
|
|||||||
<p>Choose where this credential can be used and how it signs in.</p>
|
<p>Choose where this credential can be used and how it signs in.</p>
|
||||||
</header>
|
</header>
|
||||||
<FormGrid columns={2} collapseAt="standard" className="">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="i18n:govoplan-files.credential_id.9432a6e1">
|
<FormField label="i18n:govoplan-files.credential_id.9432a6e1" helpContextId="files.connector.credentials" helpModuleId="files">
|
||||||
<input className={!editingCredentialId && !credentialDraft.id.trim() ? "field-input-missing" : undefined} aria-invalid={!editingCredentialId && !credentialDraft.id.trim() || undefined} value={credentialDraft.id} disabled={Boolean(editingCredentialId) || saving} onChange={(event) => patchCredentialDraft({ id: event.target.value })} placeholder={`${scopeType}-webdav-credentials`} />
|
<input className={!editingCredentialId && !credentialDraft.id.trim() ? "field-input-missing" : undefined} aria-invalid={!editingCredentialId && !credentialDraft.id.trim() || undefined} value={credentialDraft.id} disabled={Boolean(editingCredentialId) || saving} onChange={(event) => patchCredentialDraft({ id: event.target.value })} placeholder={`${scopeType}-webdav-credentials`} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="i18n:govoplan-files.label.74341e3c">
|
<FormField label="i18n:govoplan-files.label.74341e3c">
|
||||||
<input className={!credentialDraft.label.trim() ? "field-input-missing" : undefined} aria-invalid={!credentialDraft.label.trim() || undefined} value={credentialDraft.label} disabled={saving} onChange={(event) => patchCredentialDraft({ label: event.target.value })} placeholder="i18n:govoplan-files.govoplan_webdav_credentials.bc696d69" />
|
<input className={!credentialDraft.label.trim() ? "field-input-missing" : undefined} aria-invalid={!credentialDraft.label.trim() || undefined} value={credentialDraft.label} disabled={saving} onChange={(event) => patchCredentialDraft({ label: event.target.value })} placeholder="i18n:govoplan-files.govoplan_webdav_credentials.bc696d69" />
|
||||||
</FormField>
|
</FormField>
|
||||||
{credentialAttachProfileId &&
|
{credentialAttachProfileId &&
|
||||||
<FormField label="i18n:govoplan-files.connection_credential.178babe0">
|
<FormField label="i18n:govoplan-files.connection_credential.178babe0" helpContextId="files.connector.credentials" helpModuleId="files">
|
||||||
<input value={credentialAttachedProfile ? `${credentialAttachedProfile.label} (${credentialAttachedProfile.id})` : credentialAttachProfileId} disabled readOnly />
|
<input value={credentialAttachedProfile ? `${credentialAttachedProfile.label} (${credentialAttachedProfile.id})` : credentialAttachProfileId} disabled readOnly />
|
||||||
</FormField>
|
</FormField>
|
||||||
}
|
}
|
||||||
@@ -1130,8 +1130,8 @@ export default function FileConnectorSettingsPanel({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION}>
|
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION} helpContextId="files.connector.credentials" helpModuleId="files">
|
||||||
<select value={credentialDraft.credentialMode} disabled={saving} onChange={(event) => patchCredentialDraft({ credentialMode: event.target.value as CredentialMode })}>
|
<select data-help-context-id="files.connector.credentials" data-help-module-id="files" value={credentialDraft.credentialMode} disabled={saving} onChange={(event) => patchCredentialDraft({ credentialMode: event.target.value as CredentialMode })}>
|
||||||
<option value="none">i18n:govoplan-files.none.6eef6648</option>
|
<option value="none">i18n:govoplan-files.none.6eef6648</option>
|
||||||
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
|
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
|
||||||
<option value="basic">i18n:govoplan-files.username_password.e8ba8896</option>
|
<option value="basic">i18n:govoplan-files.username_password.e8ba8896</option>
|
||||||
@@ -1142,8 +1142,8 @@ export default function FileConnectorSettingsPanel({
|
|||||||
</FormGrid>
|
</FormGrid>
|
||||||
<div className="file-connector-settings-section">
|
<div className="file-connector-settings-section">
|
||||||
<ToggleSwitch label="i18n:govoplan-files.enabled.df174a3f" checked={credentialDraft.enabled} disabled={saving} onChange={(enabled) => patchCredentialDraft({ enabled })} />
|
<ToggleSwitch label="i18n:govoplan-files.enabled.df174a3f" checked={credentialDraft.enabled} disabled={saving} onChange={(enabled) => patchCredentialDraft({ enabled })} />
|
||||||
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_password.7442260d" checked={credentialDraft.clearPassword} disabled={saving} onChange={(clearPassword) => patchCredentialDraft({ clearPassword })} />}
|
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_password.7442260d" helpContextId="files.connector.credentials" helpModuleId="files" checked={credentialDraft.clearPassword} disabled={saving} onChange={(clearPassword) => patchCredentialDraft({ clearPassword })} />}
|
||||||
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_token.a9c670aa" checked={credentialDraft.clearToken} disabled={saving} onChange={(clearToken) => patchCredentialDraft({ clearToken })} />}
|
{editingCredentialId && <ToggleSwitch label="i18n:govoplan-files.clear_saved_token.a9c670aa" helpContextId="files.connector.credentials" helpModuleId="files" checked={credentialDraft.clearToken} disabled={saving} onChange={(clearToken) => patchCredentialDraft({ clearToken })} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -1179,8 +1179,8 @@ export default function FileConnectorSettingsPanel({
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
showPassword={false} />
|
showPassword={false} />
|
||||||
<FormGrid columns={2} collapseAt="standard" className="">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="i18n:govoplan-files.secret_reference.04ed2221">
|
<FormField label="i18n:govoplan-files.secret_reference.04ed2221" helpContextId="files.connector.credentials" helpModuleId="files">
|
||||||
<input value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
|
<input data-help-context-id="files.connector.credentials" data-help-module-id="files" value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
</>
|
</>
|
||||||
@@ -1304,8 +1304,8 @@ export default function FileConnectorSettingsPanel({
|
|||||||
<FormField label="i18n:govoplan-files.base_path.6a4867ca">
|
<FormField label="i18n:govoplan-files.base_path.6a4867ca">
|
||||||
<input value={draft.basePath} disabled={saving} onChange={(event) => patchDraft({ basePath: event.target.value })} placeholder="GovOPlaN" />
|
<input value={draft.basePath} disabled={saving} onChange={(event) => patchDraft({ basePath: event.target.value })} placeholder="GovOPlaN" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION}>
|
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION} helpContextId="files.connector.credentials" helpModuleId="files">
|
||||||
<select value={draft.credentialMode} disabled={saving} onChange={(event) => patchDraft({ credentialMode: event.target.value as CredentialMode })}>
|
<select data-help-context-id="files.connector.credentials" data-help-module-id="files" value={draft.credentialMode} disabled={saving} onChange={(event) => patchDraft({ credentialMode: event.target.value as CredentialMode })}>
|
||||||
<option value="none">i18n:govoplan-files.none.6eef6648</option>
|
<option value="none">i18n:govoplan-files.none.6eef6648</option>
|
||||||
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
|
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
|
||||||
<option value="basic">i18n:govoplan-files.username_password.e8ba8896</option>
|
<option value="basic">i18n:govoplan-files.username_password.e8ba8896</option>
|
||||||
@@ -1313,8 +1313,8 @@ export default function FileConnectorSettingsPanel({
|
|||||||
<option value="secret_ref">i18n:govoplan-files.secret_reference.04ed2221</option>
|
<option value="secret_ref">i18n:govoplan-files.secret_reference.04ed2221</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="i18n:govoplan-files.credential.8bede3ea">
|
<FormField label="i18n:govoplan-files.credential.8bede3ea" helpContextId="files.connector.credentials" helpModuleId="files">
|
||||||
<select value={profileCredentialSelectValue} disabled={saving || profileCredentialOptions.length === 0} onChange={(event) => patchDraft({ credentialProfileId: event.target.value })}>
|
<select data-help-context-id="files.connector.credentials" data-help-module-id="files" value={profileCredentialSelectValue} disabled={saving || profileCredentialOptions.length === 0} onChange={(event) => patchDraft({ credentialProfileId: event.target.value })}>
|
||||||
{profileCredentialOptions.length === 0 && <option value="">i18n:govoplan-files.no_saved_credential.30a5a951</option>}
|
{profileCredentialOptions.length === 0 && <option value="">i18n:govoplan-files.no_saved_credential.30a5a951</option>}
|
||||||
{profileCredentialOptions.map((credential) => <option key={credential.id} value={credential.id}>{credential.label}</option>)}
|
{profileCredentialOptions.map((credential) => <option key={credential.id} value={credential.id}>{credential.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -2293,7 +2293,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
<Button onClick={() => openTransferDialog("copy")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
|
<Button onClick={() => openTransferDialog("copy")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
|
||||||
{hasSelection && <Button onClick={openRenameDialog} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
|
{hasSelection && <Button onClick={openRenameDialog} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
|
||||||
<Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={Boolean(accessExplanationBlocker)} disabledReason={accessExplanationBlocker}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
|
<Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={Boolean(accessExplanationBlocker)} disabledReason={accessExplanationBlocker}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
|
||||||
<Button variant="danger" onClick={() => void deleteSelected()} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
|
<Button variant="danger" helpContextId="files.list" helpModuleId="files" onClick={() => void deleteSelected()} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
|
||||||
{activeSpaceIsConnector &&
|
{activeSpaceIsConnector &&
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -2793,8 +2793,12 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
{archivePreview.requires_password &&
|
{archivePreview.requires_password &&
|
||||||
<FormField
|
<FormField
|
||||||
label="Archive password"
|
label="Archive password"
|
||||||
|
helpContextId="files.list"
|
||||||
|
helpModuleId="files"
|
||||||
help="The password stays in this dialog and is sent only while inspecting or importing this archive.">
|
help="The password stays in this dialog and is sent only while inspecting or importing this archive.">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
helpContextId="files.list"
|
||||||
|
helpModuleId="files"
|
||||||
value={archivePassword}
|
value={archivePassword}
|
||||||
onValueChange={setArchivePassword}
|
onValueChange={setArchivePassword}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
@@ -2857,6 +2861,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
</Button>
|
</Button>
|
||||||
{archivePreview.requires_password &&
|
{archivePreview.requires_password &&
|
||||||
<Button
|
<Button
|
||||||
|
helpContextId="files.list"
|
||||||
|
helpModuleId="files"
|
||||||
onClick={() => activeDialogTarget && void loadArchivePreview(archiveFile, activeDialogTarget, { preserveSelection: true })}
|
onClick={() => activeDialogTarget && void loadArchivePreview(archiveFile, activeDialogTarget, { preserveSelection: true })}
|
||||||
disabled={busy || !archivePassword}>
|
disabled={busy || !archivePassword}>
|
||||||
<RefreshCw size={15} aria-hidden="true" /> Verify password
|
<RefreshCw size={15} aria-hidden="true" /> Verify password
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export function FileContextMenu({
|
|||||||
<button type="button" role="menuitem" onClick={onMove} disabled={!hasSelection || !canOrganize}><MoveRight size={15} aria-hidden="true" /> i18n:govoplan-files.move.8a74a26e</button>
|
<button type="button" role="menuitem" onClick={onMove} disabled={!hasSelection || !canOrganize}><MoveRight size={15} aria-hidden="true" /> i18n:govoplan-files.move.8a74a26e</button>
|
||||||
<button type="button" role="menuitem" onClick={onCopy} disabled={!hasSelection || !canOrganize}><Copy size={15} aria-hidden="true" /> i18n:govoplan-files.copy.92556c6d</button>
|
<button type="button" role="menuitem" onClick={onCopy} disabled={!hasSelection || !canOrganize}><Copy size={15} aria-hidden="true" /> i18n:govoplan-files.copy.92556c6d</button>
|
||||||
<button type="button" role="menuitem" onClick={onExplainAccess} disabled={!canExplainAccess}><KeyRound size={15} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</button>
|
<button type="button" role="menuitem" onClick={onExplainAccess} disabled={!canExplainAccess}><KeyRound size={15} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</button>
|
||||||
{showDelete && <button type="button" role="menuitem" className="danger" onClick={onDelete} disabled={!canDelete}><Trash2 size={15} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</button>}
|
{showDelete && <button type="button" role="menuitem" className="danger" data-help-context-id="files.list" data-help-module-id="files" onClick={onDelete} disabled={!canDelete}><Trash2 size={15} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</button>}
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -8,6 +8,7 @@ import {
|
|||||||
type PlatformWebModule,
|
type PlatformWebModule,
|
||||||
type QuickAccessToolsUiCapability
|
type QuickAccessToolsUiCapability
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations as productSurfaceTranslations } from "@govoplan/core-webui/outcome-product-surface-translations";
|
||||||
import { FolderTree } from "./features/files/components/FileManagerComponents";
|
import { FolderTree } from "./features/files/components/FileManagerComponents";
|
||||||
import FileConnectorSettingsPanel from "./features/files/FileConnectorSettingsPanel";
|
import FileConnectorSettingsPanel from "./features/files/FileConnectorSettingsPanel";
|
||||||
import ManagedFileChooser from "./features/files/components/ManagedFileChooser";
|
import ManagedFileChooser from "./features/files/components/ManagedFileChooser";
|
||||||
@@ -22,8 +23,8 @@ const FileIntegrityPanel = lazy(() => import("./features/files/FileIntegrityPane
|
|||||||
|
|
||||||
const fileRead = ["files:file:read"];
|
const fileRead = ["files:file:read"];
|
||||||
const translations = {
|
const translations = {
|
||||||
en: generatedTranslations.en,
|
en: { ...generatedTranslations.en, ...productSurfaceTranslations.en },
|
||||||
de: generatedTranslations.de
|
de: { ...generatedTranslations.de, ...productSurfaceTranslations.de }
|
||||||
};
|
};
|
||||||
const fileDashboardWidgets: DashboardWidgetsUiCapability = {
|
const fileDashboardWidgets: DashboardWidgetsUiCapability = {
|
||||||
widgets: [
|
widgets: [
|
||||||
|
|||||||
Reference in New Issue
Block a user