Migrate Dashboard interface patterns

This commit is contained in:
2026-08-03 14:25:02 +02:00
parent 4280e42110
commit da3947f417
9 changed files with 405 additions and 21 deletions
+34
View File
@@ -0,0 +1,34 @@
# Dashboard Interface Pattern Migration
This migration applies the GovOPlaN interface pattern language to the
view-specific summary, widget library, four-column placement grid, and widget
configuration dialog.
## Surface Inventory
| Surface | Archetype | Consequence class | Contract |
| --- | --- | --- | --- |
| `/dashboard` | Personal workspace | Inspect current task context | Stable loading, fallback, empty, error, and help states |
| Summary | Metric overview | Inspect module/widget/layout state | Installed, available, placed, active View, and persistence source remain distinguishable |
| Widget library | Capability-filtered catalogue | Add or reset placements | Permission/View filtering, search, capacity reason, drag/click parity, and reversible defaults |
| Dashboard grid | Configurable composition | Reorder, resize, configure, or remove placement | Four-column placement contract, visible drag target, guarded draft, and no provider-data mutation |
| Widget settings | Nested definition editor | Apply presentation/query settings | Guarded local draft, contextual field help, validation reason, reset, apply, and discard |
## Consequence And Availability Rules
- Layouts are scoped by tenant, account, and active focused View. A focused
View changes the composition key but never grants access to widget data.
- Configure mode edits a local draft. Save persists the complete layout using
its revision; stale concurrent saves are rejected rather than overwritten.
- Removing a widget affects only its placement. Reset restores the defaults
contributed by active modules and remains reversible until Save layout.
- Widgets are filtered by installed module, focused View surface, and declared
scopes. Every provider still authorizes its own requests.
- When the saved layout is unreachable, the browser or module defaults are
clearly identified as a fallback. Saving promotes the current draft to the
account-backed layout.
Backend and WebUI manifests publish matching page, summary, library, grid,
settings, and built-in-widget surface identifiers. English and German
catalogues cover Dashboard-owned vocabulary, contextual help resolves from
manifest documentation, and both layout and widget-setting drafts are guarded.
@@ -72,11 +72,51 @@ manifest = ModuleManifest(
routes=(FrontendRoute(path="/dashboard", component="DashboardPage", order=10),), routes=(FrontendRoute(path="/dashboard", component="DashboardPage", order=10),),
nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),), nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),),
view_surfaces=( view_surfaces=(
ViewSurface(
id="dashboard.page",
module_id="dashboard",
kind="route",
label="Dashboard",
order=10,
),
ViewSurface(
id="dashboard.summary",
module_id="dashboard",
kind="section",
label="Dashboard summary",
parent_id="dashboard.page",
order=10,
),
ViewSurface(
id="dashboard.library",
module_id="dashboard",
kind="section",
label="Widget library",
parent_id="dashboard.page",
order=20,
),
ViewSurface(
id="dashboard.grid",
module_id="dashboard",
kind="section",
label="Dashboard grid",
parent_id="dashboard.page",
order=30,
),
ViewSurface(
id="dashboard.widget-settings",
module_id="dashboard",
kind="action",
label="Widget settings",
parent_id="dashboard.grid",
order=40,
),
ViewSurface( ViewSurface(
id="dashboard.widget.installed-modules", id="dashboard.widget.installed-modules",
module_id="dashboard", module_id="dashboard",
kind="section", kind="section",
label="Installed modules widget", label="Installed modules widget",
parent_id="dashboard.grid",
order=10, order=10,
), ),
), ),
@@ -96,6 +136,53 @@ manifest = ModuleManifest(
audience=("user", "tenant_admin", "operator"), audience=("user", "tenant_admin", "operator"),
related_modules=("core", "ops"), related_modules=("core", "ops"),
order=20, order=20,
metadata={
"help_contexts": [
"dashboard.page",
"dashboard.summary",
"dashboard.library",
"dashboard.grid",
"dashboard.state.browser-fallback",
"dashboard.state.view-specific",
],
},
),
DocumentationTopic(
id="dashboard.reference.layout-and-widgets",
title="Dashboard layouts and widget consequences",
summary="Per-user, per-tenant, and per-View widget placement, sizing, configuration, availability, and fallback semantics.",
body=(
"A Dashboard layout belongs to the active tenant, account, and focused View. "
"Configuring changes only a local draft until Save layout is selected; Cancel or "
"Discard restores the last saved arrangement. Widget removal removes only the placement, "
"not the module data represented by the widget. Reset restores the defaults announced by "
"currently active modules and remains reversible until save. A widget is offered only when "
"its module, focused View surface, and permission contract are available. Widgets never grant "
"access and providers must authorize every data request. If the server layout is unreachable, "
"the browser or module default can be displayed but is identified as a fallback until saved. "
"Concurrent saves use the layout revision and reject stale updates instead of overwriting them."
),
layer="available",
documentation_types=("admin", "user"),
audience=("user", "tenant_admin", "operator", "module_admin"),
related_modules=("core", "views", "access", "ops"),
order=21,
metadata={
"help_contexts": [
"dashboard.action.configure",
"dashboard.action.save",
"dashboard.action.reset",
"dashboard.action.remove-widget",
"dashboard.field.widget-size",
"dashboard.field.widget-configuration",
],
"consequence_classes": {
"save_layout": "Persists the complete layout for the active tenant, account, and View revision context.",
"reset_layout": "Replaces the draft with current module defaults and remains reversible until save.",
"remove_widget": "Removes only the placement from the draft; provider data is unchanged.",
"configure_widget": "Changes presentation and query preferences for one widget placement.",
},
},
), ),
), ),
architecture=declared_module_architecture( architecture=declared_module_architecture(
@@ -0,0 +1,62 @@
from __future__ import annotations
from pathlib import Path
import unittest
from govoplan_dashboard.backend.manifest import get_manifest
REPO_ROOT = Path(__file__).resolve().parents[1]
class DashboardInterfaceDocumentationContractTests(unittest.TestCase):
def test_surface_hierarchy_remains_declared(self) -> None:
frontend = get_manifest().frontend
self.assertIsNotNone(frontend)
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
self.assertEqual(
{
"dashboard.page",
"dashboard.summary",
"dashboard.library",
"dashboard.grid",
"dashboard.widget-settings",
"dashboard.widget.installed-modules",
},
set(surfaces),
)
self.assertEqual("dashboard.page", surfaces["dashboard.library"].parent_id)
self.assertEqual("dashboard.page", surfaces["dashboard.grid"].parent_id)
self.assertEqual("dashboard.grid", surfaces["dashboard.widget-settings"].parent_id)
def test_help_and_consequence_metadata_remain_published(self) -> None:
topics = {topic.id: topic for topic in get_manifest().documentation}
home = topics["dashboard.configurable-home"]
reference = topics["dashboard.reference.layout-and-widgets"]
self.assertIn("dashboard.state.browser-fallback", home.metadata["help_contexts"])
self.assertIn("dashboard.field.widget-size", reference.metadata["help_contexts"])
self.assertIn("save_layout", reference.metadata["consequence_classes"])
self.assertIn("remove_widget", reference.metadata["consequence_classes"])
def test_webui_guards_layout_and_nested_widget_drafts(self) -> None:
page = (REPO_ROOT / "webui/src/features/dashboard/DashboardPage.tsx").read_text(
encoding="utf-8"
)
dialog = (
REPO_ROOT
/ "webui/src/features/dashboard/WidgetConfigurationDialog.tsx"
).read_text(encoding="utf-8")
library = (
REPO_ROOT / "webui/src/features/dashboard/WidgetLibrary.tsx"
).read_text(encoding="utf-8")
self.assertIn("DocumentationHelpLink", page)
self.assertIn("useUnsavedDraftGuard", page)
self.assertIn("useUnsavedDraftGuard", dialog)
self.assertIn("DASHBOARD_LAYOUT_DOCUMENTATION", dialog)
self.assertIn("disabled={atCapacity}", library)
if __name__ == "__main__":
unittest.main()
+24 -5
View File
@@ -13,6 +13,7 @@ import {
import { import {
Button, Button,
DismissibleAlert, DismissibleAlert,
DocumentationHelpLink,
LoadingFrame, LoadingFrame,
MetricCard, MetricCard,
PageScrollViewport, PageScrollViewport,
@@ -36,6 +37,10 @@ import {
import DashboardGrid from "./DashboardGrid"; import DashboardGrid from "./DashboardGrid";
import WidgetConfigurationDialog from "./WidgetConfigurationDialog"; import WidgetConfigurationDialog from "./WidgetConfigurationDialog";
import WidgetLibrary from "./WidgetLibrary"; import WidgetLibrary from "./WidgetLibrary";
import {
DASHBOARD_DOCUMENTATION,
DASHBOARD_I18N
} from "./interfacePatterns";
import { import {
appendPlacement, appendPlacement,
DASHBOARD_COLUMN_COUNT, DASHBOARD_COLUMN_COUNT,
@@ -184,8 +189,8 @@ export default function DashboardPage({
dirty, dirty,
onSave: persistLayout, onSave: persistLayout,
onDiscard: discardChanges, onDiscard: discardChanges,
title: "Unsaved Dashboard layout", title: "i18n:govoplan-dashboard.unsaved_layout_title",
message: "Save or discard the Dashboard arrangement before leaving this page." message: "i18n:govoplan-dashboard.unsaved_layout_message"
}); });
const activeLayout = configuring ? draftLayout : savedLayout; const activeLayout = configuring ? draftLayout : savedLayout;
@@ -446,25 +451,39 @@ export default function DashboardPage({
<p>Personal workspace assembled from installed module widgets.</p> <p>Personal workspace assembled from installed module widgets.</p>
</div> </div>
<div className="button-row compact-actions"> <div className="button-row compact-actions">
<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />
{!configuring && ( {!configuring && (
<> <>
<Button onClick={() => setRefreshKey((value) => value + 1)}> <Button
onClick={() => setRefreshKey((value) => value + 1)}
disabled={loading}
disabledReason={loading ? DASHBOARD_I18N.loading : undefined}
>
<RefreshCw size={16} /> Refresh <RefreshCw size={16} /> Refresh
</Button> </Button>
<Button onClick={beginConfiguration}> <Button
onClick={beginConfiguration}
disabled={loading}
disabledReason={loading ? DASHBOARD_I18N.loading : undefined}
>
<SlidersHorizontal size={16} /> Configure <SlidersHorizontal size={16} /> Configure
</Button> </Button>
</> </>
)} )}
{configuring && ( {configuring && (
<> <>
<Button onClick={discardChanges} disabled={saving}> <Button
onClick={discardChanges}
disabled={saving}
disabledReason={saving ? DASHBOARD_I18N.saving : undefined}
>
<X size={16} /> Cancel <X size={16} /> Cancel
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
onClick={() => void persistLayout()} onClick={() => void persistLayout()}
disabled={saving || !dirty} disabled={saving || !dirty}
disabledReason={saving ? DASHBOARD_I18N.saving : !dirty ? DASHBOARD_I18N.noChanges : undefined}
> >
<Save size={16} /> {saving ? "Saving..." : "Save layout"} <Save size={16} /> {saving ? "Saving..." : "Save layout"}
</Button> </Button>
@@ -2,9 +2,12 @@ import { useEffect, useMemo, useState } from "react";
import { import {
Button, Button,
Dialog, Dialog,
DocumentationHelpLink,
FormField, FormField,
SegmentedControl, SegmentedControl,
ToggleSwitch, ToggleSwitch,
useUnsavedChanges,
useUnsavedDraftGuard,
type DashboardWidgetConfiguration, type DashboardWidgetConfiguration,
type DashboardWidgetConfigurationField, type DashboardWidgetConfigurationField,
type DashboardWidgetConfigurationValue, type DashboardWidgetConfigurationValue,
@@ -16,6 +19,10 @@ import {
supportedWidgetSizes, supportedWidgetSizes,
type DashboardWidgetPlacement type DashboardWidgetPlacement
} from "./dashboardLayout"; } from "./dashboardLayout";
import {
DASHBOARD_I18N,
DASHBOARD_LAYOUT_DOCUMENTATION
} from "./interfacePatterns";
type WidgetConfigurationDialogProps = { type WidgetConfigurationDialogProps = {
open: boolean; open: boolean;
@@ -32,8 +39,25 @@ export default function WidgetConfigurationDialog({
onClose, onClose,
onSave onSave
}: WidgetConfigurationDialogProps) { }: WidgetConfigurationDialogProps) {
const { requestDiscard } = useUnsavedChanges();
const [size, setSize] = useState<DashboardWidgetSize>("medium"); const [size, setSize] = useState<DashboardWidgetSize>("medium");
const [configuration, setConfiguration] = useState<DashboardWidgetConfiguration>({}); const [configuration, setConfiguration] = useState<DashboardWidgetConfiguration>({});
const savedConfiguration = useMemo(
() => ({
...(widget?.defaultConfiguration ?? {}),
...(placement?.configuration ?? {})
}),
[placement, widget]
);
const dirty = Boolean(
open
&& widget
&& placement
&& (
size !== placement.size
|| JSON.stringify(configuration) !== JSON.stringify(savedConfiguration)
)
);
useEffect(() => { useEffect(() => {
if (!open || !widget || !placement) return; if (!open || !widget || !placement) return;
@@ -58,6 +82,34 @@ export default function WidgetConfigurationDialog({
[configuration, widget] [configuration, widget]
); );
function discardDraft() {
if (!widget || !placement) return;
setSize(placement.size);
setConfiguration({
...(widget.defaultConfiguration ?? {}),
...placement.configuration
});
}
function applyDraft(): boolean {
if (!placement || invalidConfiguration) return false;
onSave({ ...placement, size, configuration });
return true;
}
useUnsavedDraftGuard({
dirty,
title: "i18n:govoplan-dashboard.unsaved_widget_title",
message: "i18n:govoplan-dashboard.unsaved_widget_message",
onSave: applyDraft,
onDiscard: discardDraft
});
function close() {
if (dirty) requestDiscard(onClose);
else onClose();
}
if (!widget || !placement) return null; if (!widget || !placement) return null;
const supportedSizes = supportedWidgetSizes(widget); const supportedSizes = supportedWidgetSizes(widget);
@@ -74,26 +126,28 @@ export default function WidgetConfigurationDialog({
<Dialog <Dialog
open={open} open={open}
title={`Configure ${widget.title}`} title={`Configure ${widget.title}`}
onClose={onClose} onClose={close}
className="dashboard-widget-config-dialog" className="dashboard-widget-config-dialog"
footer={ footer={
<> <>
<Button type="button" variant="ghost" onClick={reset}>Reset</Button> <Button type="button" variant="ghost" onClick={reset}>Reset</Button>
<span className="dialog-footer-spacer" /> <span className="dialog-footer-spacer" />
<Button type="button" onClick={onClose}>Cancel</Button> <Button type="button" onClick={close}>Cancel</Button>
<Button <Button
type="button" type="button"
variant="primary" variant="primary"
disabled={invalidConfiguration} disabled={invalidConfiguration}
onClick={() => onSave({ ...placement, size, configuration })} disabledReason={invalidConfiguration ? DASHBOARD_I18N.invalidWidget : undefined}
onClick={applyDraft}
> >
Apply Apply
</Button> </Button>
</> </>
} }
> >
<DocumentationHelpLink reference={DASHBOARD_LAYOUT_DOCUMENTATION} />
{supportedSizes.length > 1 && ( {supportedSizes.length > 1 && (
<FormField label="Widget size"> <FormField label="Widget size" documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
<SegmentedControl <SegmentedControl
options={supportedSizes.map((candidate) => ({ options={supportedSizes.map((candidate) => ({
id: candidate, id: candidate,
@@ -142,7 +196,7 @@ function ConfigurationField({
if (field.kind === "select") { if (field.kind === "select") {
return ( return (
<FormField label={field.label} help={field.description}> <FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
<select <select
value={typeof value === "string" ? value : ""} value={typeof value === "string" ? value : ""}
required={field.required} required={field.required}
@@ -161,7 +215,7 @@ function ConfigurationField({
if (field.kind === "number") { if (field.kind === "number") {
return ( return (
<FormField label={field.label} help={field.description}> <FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
<input <input
type="number" type="number"
value={typeof value === "number" ? value : ""} value={typeof value === "number" ? value : ""}
@@ -180,7 +234,7 @@ function ConfigurationField({
} }
return ( return (
<FormField label={field.label} help={field.description}> <FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
<input <input
type="text" type="text"
value={typeof value === "string" ? value : ""} value={typeof value === "string" ? value : ""}
@@ -66,6 +66,7 @@ export default function WidgetLibrary({
label={`Add ${widget.title}`} label={`Add ${widget.title}`}
icon={<Plus size={16} />} icon={<Plus size={16} />}
variant="ghost" variant="ghost"
disabled={atCapacity}
disabledReason={ disabledReason={
atCapacity atCapacity
? "A Dashboard can contain at most 100 widgets." ? "A Dashboard can contain at most 100 widgets."
@@ -0,0 +1,18 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const DASHBOARD_DOCUMENTATION = {
topicId: "dashboard.configurable-home",
documentationType: "user"
} satisfies DocumentationHelpReference;
export const DASHBOARD_LAYOUT_DOCUMENTATION = {
topicId: "dashboard.reference.layout-and-widgets",
documentationType: "user"
} satisfies DocumentationHelpReference;
export const DASHBOARD_I18N = {
loading: "i18n:govoplan-dashboard.loading_reason",
saving: "i18n:govoplan-dashboard.saving_reason",
noChanges: "i18n:govoplan-dashboard.no_changes_reason",
invalidWidget: "i18n:govoplan-dashboard.invalid_widget_reason"
} as const;
+71 -7
View File
@@ -1,9 +1,73 @@
export const generatedTranslations = { import type { PlatformTranslations } from "@govoplan/core-webui";
en: {
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard" const en = {
}, "i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard",
de: { "i18n:govoplan-dashboard.summary": "Dashboard summary",
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard" "i18n:govoplan-dashboard.library": "Widget library",
} "i18n:govoplan-dashboard.grid": "Dashboard grid",
"i18n:govoplan-dashboard.widget_settings": "Widget settings",
"i18n:govoplan-dashboard.installed_modules_widget": "Installed modules widget",
"i18n:govoplan-dashboard.loading_reason": "The saved Dashboard layout is still loading.",
"i18n:govoplan-dashboard.saving_reason": "The Dashboard layout is still being saved.",
"i18n:govoplan-dashboard.no_changes_reason": "There are no unsaved Dashboard changes.",
"i18n:govoplan-dashboard.invalid_widget_reason": "Complete the required widget settings with values inside their allowed ranges.",
"i18n:govoplan-dashboard.unsaved_layout_title": "Unsaved Dashboard layout",
"i18n:govoplan-dashboard.unsaved_layout_message": "Save or discard the Dashboard arrangement before leaving this page.",
"i18n:govoplan-dashboard.unsaved_widget_title": "Unapplied widget settings",
"i18n:govoplan-dashboard.unsaved_widget_message": "Apply or discard the widget settings before leaving this dialog.",
"Personal workspace assembled from installed module widgets.": "Personal workspace assembled from installed module widgets.",
"Refresh": "Refresh",
"Configure": "Configure",
"Cancel": "Cancel",
"Save layout": "Save layout",
"Saving...": "Saving...",
"Installed modules": "Installed modules",
"Available widgets": "Available widgets",
"On dashboard": "On dashboard",
"Layout": "Layout",
"Widget library": "Widget library",
"Search widgets": "Search widgets",
"Reset to module defaults": "Reset to module defaults",
"Widget size": "Widget size",
"Reset": "Reset",
"Apply": "Apply",
"Select an option": "Select an option",
"None": "None"
} as const;
const de: Record<keyof typeof en, string> = {
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Übersicht",
"i18n:govoplan-dashboard.summary": "Übersichtszusammenfassung",
"i18n:govoplan-dashboard.library": "Widget-Bibliothek",
"i18n:govoplan-dashboard.grid": "Übersichtsraster",
"i18n:govoplan-dashboard.widget_settings": "Widget-Einstellungen",
"i18n:govoplan-dashboard.installed_modules_widget": "Widget für installierte Module",
"i18n:govoplan-dashboard.loading_reason": "Die gespeicherte Übersichtsanordnung wird noch geladen.",
"i18n:govoplan-dashboard.saving_reason": "Die Übersichtsanordnung wird noch gespeichert.",
"i18n:govoplan-dashboard.no_changes_reason": "Es gibt keine ungespeicherten Änderungen an der Übersicht.",
"i18n:govoplan-dashboard.invalid_widget_reason": "Füllen Sie die erforderlichen Widget-Einstellungen mit Werten innerhalb der erlaubten Bereiche aus.",
"i18n:govoplan-dashboard.unsaved_layout_title": "Ungespeicherte Übersichtsanordnung",
"i18n:govoplan-dashboard.unsaved_layout_message": "Speichern oder verwerfen Sie die Übersichtsanordnung, bevor Sie diese Seite verlassen.",
"i18n:govoplan-dashboard.unsaved_widget_title": "Nicht angewendete Widget-Einstellungen",
"i18n:govoplan-dashboard.unsaved_widget_message": "Wenden Sie die Widget-Einstellungen an oder verwerfen Sie sie, bevor Sie den Dialog verlassen.",
"Personal workspace assembled from installed module widgets.": "Persönlicher Arbeitsbereich aus Widgets der installierten Module.",
"Refresh": "Aktualisieren",
"Configure": "Konfigurieren",
"Cancel": "Abbrechen",
"Save layout": "Anordnung speichern",
"Saving...": "Speichert...",
"Installed modules": "Installierte Module",
"Available widgets": "Verfügbare Widgets",
"On dashboard": "Auf der Übersicht",
"Layout": "Anordnung",
"Widget library": "Widget-Bibliothek",
"Search widgets": "Widgets suchen",
"Reset to module defaults": "Auf Modulvorgaben zurücksetzen",
"Widget size": "Widget-Größe",
"Reset": "Zurücksetzen",
"Apply": "Anwenden",
"Select an option": "Option auswählen",
"None": "Keine"
}; };
export const generatedTranslations: PlatformTranslations = { en, de };
+47 -2
View File
@@ -69,17 +69,62 @@ export const dashboardModule: PlatformWebModule = {
optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"], optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"],
translations, translations,
viewSurfaces: [ viewSurfaces: [
{
id: "dashboard.page",
moduleId: "dashboard",
kind: "route",
label: "i18n:govoplan-dashboard.dashboard.3f8b4df2",
order: 10
},
{
id: "dashboard.summary",
moduleId: "dashboard",
kind: "section",
label: "i18n:govoplan-dashboard.summary",
parentId: "dashboard.page",
order: 10
},
{
id: "dashboard.library",
moduleId: "dashboard",
kind: "section",
label: "i18n:govoplan-dashboard.library",
parentId: "dashboard.page",
order: 20
},
{
id: "dashboard.grid",
moduleId: "dashboard",
kind: "section",
label: "i18n:govoplan-dashboard.grid",
parentId: "dashboard.page",
order: 30
},
{
id: "dashboard.widget-settings",
moduleId: "dashboard",
kind: "action",
label: "i18n:govoplan-dashboard.widget_settings",
parentId: "dashboard.grid",
order: 40
},
{ {
id: "dashboard.widget.installed-modules", id: "dashboard.widget.installed-modules",
moduleId: "dashboard", moduleId: "dashboard",
kind: "section", kind: "section",
label: "Installed modules widget", label: "i18n:govoplan-dashboard.installed_modules_widget",
parentId: "dashboard.grid",
order: 10 order: 10
} }
], ],
navItems: [{ to: "/dashboard", label: "i18n:govoplan-dashboard.dashboard.3f8b4df2", iconName: "dashboard", order: 10 }], navItems: [{ to: "/dashboard", label: "i18n:govoplan-dashboard.dashboard.3f8b4df2", iconName: "dashboard", order: 10 }],
routes: [ routes: [
{ path: "/dashboard", order: 10, render: ({ settings, auth }) => createElement(DashboardPage, { settings, auth }) } {
path: "/dashboard",
order: 10,
surfaceId: "dashboard.page",
render: ({ settings, auth }) => createElement(DashboardPage, { settings, auth })
}
], ],
uiCapabilities: { uiCapabilities: {
"dashboard.widgets": dashboardWidgets "dashboard.widgets": dashboardWidgets