Add dashboard module
This commit is contained in:
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# GovOPlaN Dashboard
|
||||||
|
|
||||||
|
Configurable dashboard module for GovOPlaN.
|
||||||
|
|
||||||
|
The module owns the `/dashboard` route when installed. Core keeps only a minimal
|
||||||
|
fallback home for installations where this module is absent.
|
||||||
|
|
||||||
|
Modules contribute widgets through the `dashboard.widgets` WebUI capability.
|
||||||
|
The first implementation stores personal widget visibility in browser storage;
|
||||||
|
server-side layout persistence can be added later without changing the widget
|
||||||
|
contract.
|
||||||
|
|
||||||
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/dashboard-webui",
|
||||||
|
"version": "0.1.6",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "webui/src/index.ts",
|
||||||
|
"module": "webui/src/index.ts",
|
||||||
|
"types": "webui/src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./webui/src/index.ts",
|
||||||
|
"import": "./webui/src/index.ts"
|
||||||
|
},
|
||||||
|
"./styles/dashboard.css": "./webui/src/styles/dashboard.css"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"webui/src",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.6",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "govoplan-dashboard"
|
||||||
|
version = "0.1.6"
|
||||||
|
description = "GovOPlaN configurable dashboard module."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
authors = [{ name = "GovOPlaN" }]
|
||||||
|
dependencies = [
|
||||||
|
"govoplan-core>=0.1.6",
|
||||||
|
"govoplan-access>=0.1.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
govoplan_dashboard = ["py.typed"]
|
||||||
|
|
||||||
|
[project.entry-points."govoplan.modules"]
|
||||||
|
dashboard = "govoplan_dashboard.backend.manifest:get_manifest"
|
||||||
|
|
||||||
22
src/govoplan_dashboard.egg-info/PKG-INFO
Normal file
22
src/govoplan_dashboard.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: govoplan-dashboard
|
||||||
|
Version: 0.1.6
|
||||||
|
Summary: GovOPlaN configurable dashboard module.
|
||||||
|
Author: GovOPlaN
|
||||||
|
Requires-Python: >=3.12
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
Requires-Dist: govoplan-core>=0.1.6
|
||||||
|
Requires-Dist: govoplan-access>=0.1.6
|
||||||
|
|
||||||
|
# GovOPlaN Dashboard
|
||||||
|
|
||||||
|
Configurable dashboard module for GovOPlaN.
|
||||||
|
|
||||||
|
The module owns the `/dashboard` route when installed. Core keeps only a minimal
|
||||||
|
fallback home for installations where this module is absent.
|
||||||
|
|
||||||
|
Modules contribute widgets through the `dashboard.widgets` WebUI capability.
|
||||||
|
The first implementation stores personal widget visibility in browser storage;
|
||||||
|
server-side layout persistence can be added later without changing the widget
|
||||||
|
contract.
|
||||||
|
|
||||||
12
src/govoplan_dashboard.egg-info/SOURCES.txt
Normal file
12
src/govoplan_dashboard.egg-info/SOURCES.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
src/govoplan_dashboard/__init__.py
|
||||||
|
src/govoplan_dashboard/py.typed
|
||||||
|
src/govoplan_dashboard.egg-info/PKG-INFO
|
||||||
|
src/govoplan_dashboard.egg-info/SOURCES.txt
|
||||||
|
src/govoplan_dashboard.egg-info/dependency_links.txt
|
||||||
|
src/govoplan_dashboard.egg-info/entry_points.txt
|
||||||
|
src/govoplan_dashboard.egg-info/requires.txt
|
||||||
|
src/govoplan_dashboard.egg-info/top_level.txt
|
||||||
|
src/govoplan_dashboard/backend/__init__.py
|
||||||
|
src/govoplan_dashboard/backend/manifest.py
|
||||||
1
src/govoplan_dashboard.egg-info/dependency_links.txt
Normal file
1
src/govoplan_dashboard.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
src/govoplan_dashboard.egg-info/entry_points.txt
Normal file
2
src/govoplan_dashboard.egg-info/entry_points.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[govoplan.modules]
|
||||||
|
dashboard = govoplan_dashboard.backend.manifest:get_manifest
|
||||||
2
src/govoplan_dashboard.egg-info/requires.txt
Normal file
2
src/govoplan_dashboard.egg-info/requires.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
govoplan-core>=0.1.6
|
||||||
|
govoplan-access>=0.1.6
|
||||||
1
src/govoplan_dashboard.egg-info/top_level.txt
Normal file
1
src/govoplan_dashboard.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
govoplan_dashboard
|
||||||
6
src/govoplan_dashboard/__init__.py
Normal file
6
src/govoplan_dashboard/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""GovOPlaN dashboard module."""
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
|
__version__ = "0.1.6"
|
||||||
|
|
||||||
BIN
src/govoplan_dashboard/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/govoplan_dashboard/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
2
src/govoplan_dashboard/backend/__init__.py
Normal file
2
src/govoplan_dashboard/backend/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Backend integration for the GovOPlaN dashboard module."""
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
40
src/govoplan_dashboard/backend/manifest.py
Normal file
40
src/govoplan_dashboard/backend/manifest.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from govoplan_core.core.modules import DocumentationTopic, FrontendModule, FrontendRoute, ModuleManifest, NavItem
|
||||||
|
|
||||||
|
|
||||||
|
manifest = ModuleManifest(
|
||||||
|
id="dashboard",
|
||||||
|
name="Dashboard",
|
||||||
|
version="0.1.6",
|
||||||
|
dependencies=("access",),
|
||||||
|
optional_dependencies=("ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"),
|
||||||
|
nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),),
|
||||||
|
frontend=FrontendModule(
|
||||||
|
module_id="dashboard",
|
||||||
|
package_name="@govoplan/dashboard-webui",
|
||||||
|
routes=(FrontendRoute(path="/dashboard", component="DashboardPage", order=10),),
|
||||||
|
nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),),
|
||||||
|
),
|
||||||
|
documentation=(
|
||||||
|
DocumentationTopic(
|
||||||
|
id="dashboard.configurable-home",
|
||||||
|
title="Configurable user dashboard",
|
||||||
|
summary="The dashboard module owns the configurable home surface. Feature modules expose widgets through a narrow dashboard.widgets capability.",
|
||||||
|
body=(
|
||||||
|
"Core only provides a minimal fallback home when the dashboard module is absent. "
|
||||||
|
"Dashboard widgets must be contributed through core contracts, not by importing sibling module components directly."
|
||||||
|
),
|
||||||
|
layer="configured",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("user", "tenant_admin", "operator"),
|
||||||
|
related_modules=("core", "ops"),
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_manifest() -> ModuleManifest:
|
||||||
|
return manifest
|
||||||
|
|
||||||
1
src/govoplan_dashboard/py.typed
Normal file
1
src/govoplan_dashboard/py.typed
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
29
webui/package.json
Normal file
29
webui/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/dashboard-webui",
|
||||||
|
"version": "0.1.6",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"./styles/dashboard.css": "./src/styles/dashboard.css"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.6",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
186
webui/src/features/dashboard/DashboardPage.tsx
Normal file
186
webui/src/features/dashboard/DashboardPage.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { RefreshCw, RotateCcw, SlidersHorizontal } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
MetricCard,
|
||||||
|
PageTitle,
|
||||||
|
dashboardWidgetsForModules,
|
||||||
|
hasAnyScope,
|
||||||
|
hasScope,
|
||||||
|
usePlatformModules,
|
||||||
|
type ApiSettings,
|
||||||
|
type AuthInfo,
|
||||||
|
type DashboardWidgetContribution,
|
||||||
|
type DashboardWidgetSize
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
type DashboardLayout = {
|
||||||
|
visible: string[];
|
||||||
|
known: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DashboardPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||||
|
const modules = usePlatformModules();
|
||||||
|
const widgets = useMemo(() => dashboardWidgetsForModules(modules).filter((widget) => canUseWidget(auth, widget)), [auth, modules]);
|
||||||
|
const widgetSignature = widgets.map((widget) => widget.id).join("|");
|
||||||
|
const storageKey = `govoplan.dashboard.layout:${auth.active_tenant?.id ?? auth.tenant.id}:${auth.user.id}`;
|
||||||
|
const [layout, setLayout] = useState<DashboardLayout>(() => defaultLayout(widgets));
|
||||||
|
const [configuring, setConfiguring] = useState(false);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLayout(readLayout(storageKey) ?? defaultLayout(widgets));
|
||||||
|
}, [storageKey, widgetSignature]);
|
||||||
|
|
||||||
|
const visibleWidgetIds = useMemo(() => {
|
||||||
|
const known = new Set(layout.known);
|
||||||
|
const visible = new Set(layout.visible);
|
||||||
|
for (const widget of widgets) {
|
||||||
|
if (!known.has(widget.id) && widget.defaultVisible !== false) visible.add(widget.id);
|
||||||
|
}
|
||||||
|
const availableIds = new Set(widgets.map((widget) => widget.id));
|
||||||
|
return widgets.map((widget) => widget.id).filter((id) => availableIds.has(id) && visible.has(id));
|
||||||
|
}, [layout, widgets]);
|
||||||
|
const visibleWidgets = widgets.filter((widget) => visibleWidgetIds.includes(widget.id));
|
||||||
|
const hiddenCount = Math.max(widgets.length - visibleWidgets.length, 0);
|
||||||
|
|
||||||
|
function saveLayout(next: DashboardLayout) {
|
||||||
|
setLayout(next);
|
||||||
|
writeLayout(storageKey, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWidgetVisible(widgetId: string, visible: boolean) {
|
||||||
|
const allIds = widgets.map((widget) => widget.id);
|
||||||
|
const nextVisible = new Set(visibleWidgetIds);
|
||||||
|
if (visible) {
|
||||||
|
nextVisible.add(widgetId);
|
||||||
|
} else {
|
||||||
|
nextVisible.delete(widgetId);
|
||||||
|
}
|
||||||
|
saveLayout({
|
||||||
|
known: allIds,
|
||||||
|
visible: allIds.filter((id) => nextVisible.has(id))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLayout() {
|
||||||
|
saveLayout(defaultLayout(widgets));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content-pad workspace-data-page dashboard-page">
|
||||||
|
<div className="page-heading split workspace-heading">
|
||||||
|
<div>
|
||||||
|
<PageTitle>i18n:govoplan-dashboard.dashboard.3f8b4df2</PageTitle>
|
||||||
|
<p>Personal workspace assembled from installed module widgets.</p>
|
||||||
|
</div>
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<Button onClick={() => setRefreshKey((value) => value + 1)}><RefreshCw size={16} /> Refresh</Button>
|
||||||
|
<Button onClick={() => setConfiguring((value) => !value)}><SlidersHorizontal size={16} /> Configure</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metric-grid dashboard-summary-grid">
|
||||||
|
<MetricCard label="Installed modules" value={modules.length} tone="info" detail={moduleLabels(modules)} />
|
||||||
|
<MetricCard label="Available widgets" value={widgets.length} tone="neutral" detail="Provided by active modules" />
|
||||||
|
<MetricCard label="Visible widgets" value={visibleWidgets.length} tone="good" detail={hiddenCount ? `${hiddenCount} hidden` : "All shown"} />
|
||||||
|
<MetricCard label="Layout" value="personal" tone="neutral" detail="Saved in this browser" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{configuring &&
|
||||||
|
<Card
|
||||||
|
title="Dashboard widgets"
|
||||||
|
actions={<Button onClick={resetLayout}><RotateCcw size={16} /> Reset</Button>}>
|
||||||
|
{widgets.length === 0 ? <p className="muted">No installed module exposes dashboard widgets yet.</p> :
|
||||||
|
<div className="dashboard-widget-picker">
|
||||||
|
{widgets.map((widget) =>
|
||||||
|
<label key={widget.id} className="dashboard-widget-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={visibleWidgetIds.includes(widget.id)}
|
||||||
|
onChange={(event) => setWidgetVisible(widget.id, event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<strong>{widget.title}</strong>
|
||||||
|
<small>{widget.description ?? widget.id}</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
|
||||||
|
{visibleWidgets.length === 0 ?
|
||||||
|
<Card title="No widgets selected">
|
||||||
|
<p className="muted">Open Configure and select at least one widget. Modules can add more widgets by exposing the dashboard.widgets capability.</p>
|
||||||
|
</Card> :
|
||||||
|
<div className="dashboard-widget-grid">
|
||||||
|
{visibleWidgets.map((widget) =>
|
||||||
|
<section key={widget.id} className={`dashboard-widget dashboard-widget-${widgetSize(widget)}`}>
|
||||||
|
<Card title={widget.title} collapsible collapseKey={`dashboard-widget:${widget.id}`}>
|
||||||
|
{widget.render({
|
||||||
|
settings,
|
||||||
|
auth,
|
||||||
|
modules,
|
||||||
|
widgetId: widget.id,
|
||||||
|
refreshKey,
|
||||||
|
size: widgetSize(widget)
|
||||||
|
})}
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseWidget(auth: AuthInfo, widget: DashboardWidgetContribution): boolean {
|
||||||
|
if (widget.allOf?.length && !widget.allOf.every((scope) => hasScope(auth, scope))) return false;
|
||||||
|
if (widget.anyOf?.length && !hasAnyScope(auth, widget.anyOf)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetSize(widget: DashboardWidgetContribution): DashboardWidgetSize {
|
||||||
|
return widget.defaultSize ?? "medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultLayout(widgets: DashboardWidgetContribution[]): DashboardLayout {
|
||||||
|
const ids = widgets.map((widget) => widget.id);
|
||||||
|
return {
|
||||||
|
known: ids,
|
||||||
|
visible: widgets.filter((widget) => widget.defaultVisible !== false).map((widget) => widget.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLayout(storageKey: string): DashboardLayout | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(storageKey);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as Partial<DashboardLayout>;
|
||||||
|
if (!Array.isArray(parsed.visible) || !Array.isArray(parsed.known)) return null;
|
||||||
|
return {
|
||||||
|
visible: parsed.visible.filter((id): id is string => typeof id === "string"),
|
||||||
|
known: parsed.known.filter((id): id is string => typeof id === "string")
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLayout(storageKey: string, layout: DashboardLayout): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, JSON.stringify(layout));
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable in restricted browsing contexts.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleLabels(modules: Array<{ id: string }>): string {
|
||||||
|
if (!modules.length) return "Core shell only";
|
||||||
|
return modules.slice(0, 4).map((module) => module.id).join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { StatusBadge, type PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export default function InstalledModulesWidget({ modules }: { modules: PlatformWebModule[] }) {
|
||||||
|
if (!modules.length) return <p className="muted">No WebUI modules are active.</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="detail-list dashboard-compact-list">
|
||||||
|
{modules.map((module) =>
|
||||||
|
<div key={module.id}>
|
||||||
|
<dt><StatusBadge status="success" label={module.id} /></dt>
|
||||||
|
<dd><strong>{module.label}</strong><span className="muted"> v{module.version}</span></dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>);
|
||||||
|
}
|
||||||
|
|
||||||
9
webui/src/i18n/generatedTranslations.ts
Normal file
9
webui/src/i18n/generatedTranslations.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const generatedTranslations = {
|
||||||
|
en: {
|
||||||
|
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
6
webui/src/index.ts
Normal file
6
webui/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { default } from "./module";
|
||||||
|
export * from "./module";
|
||||||
|
export { default as DashboardPage } from "./features/dashboard/DashboardPage";
|
||||||
|
export { default as InstalledModulesWidget } from "./features/dashboard/widgets/InstalledModulesWidget";
|
||||||
|
export type { DashboardWidgetContribution, DashboardWidgetsUiCapability } from "@govoplan/core-webui";
|
||||||
|
|
||||||
46
webui/src/module.ts
Normal file
46
webui/src/module.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { createElement, lazy } from "react";
|
||||||
|
import type { DashboardWidgetsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
import InstalledModulesWidget from "./features/dashboard/widgets/InstalledModulesWidget";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
|
import "./styles/dashboard.css";
|
||||||
|
|
||||||
|
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
|
||||||
|
|
||||||
|
const dashboardWidgets: DashboardWidgetsUiCapability = {
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
id: "dashboard.installed-modules",
|
||||||
|
title: "Installed modules",
|
||||||
|
description: "Enabled WebUI modules in this browser session.",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
category: "Platform",
|
||||||
|
order: 10,
|
||||||
|
defaultSize: "medium",
|
||||||
|
render: ({ modules }) => createElement(InstalledModulesWidget, { modules })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: generatedTranslations.en,
|
||||||
|
de: generatedTranslations.de
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dashboardModule: PlatformWebModule = {
|
||||||
|
id: "dashboard",
|
||||||
|
label: "i18n:govoplan-dashboard.dashboard.3f8b4df2",
|
||||||
|
version: "1.0.0",
|
||||||
|
dependencies: ["access"],
|
||||||
|
optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"],
|
||||||
|
translations,
|
||||||
|
navItems: [{ to: "/dashboard", label: "i18n:govoplan-dashboard.dashboard.3f8b4df2", iconName: "dashboard", order: 10 }],
|
||||||
|
routes: [
|
||||||
|
{ path: "/dashboard", order: 10, render: ({ settings, auth }) => createElement(DashboardPage, { settings, auth }) }
|
||||||
|
],
|
||||||
|
uiCapabilities: {
|
||||||
|
"dashboard.widgets": dashboardWidgets
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboardModule;
|
||||||
|
|
||||||
97
webui/src/styles/dashboard.css
Normal file
97
webui/src/styles/dashboard.css
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.dashboard-page .card + .dashboard-widget-grid {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-summary-grid .metric-detail {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-small {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-medium {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-wide,
|
||||||
|
.dashboard-widget-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-picker {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-toggle {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--panel-soft);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-toggle input {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-toggle strong,
|
||||||
|
.dashboard-widget-toggle small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-toggle small {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-compact-list.detail-list div {
|
||||||
|
grid-template-columns: minmax(92px, auto) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-metrics.metric-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1120px) {
|
||||||
|
.dashboard-widget-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.dashboard-widget-grid,
|
||||||
|
.dashboard-widget-picker,
|
||||||
|
.dashboard-widget-metrics.metric-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-widget-small,
|
||||||
|
.dashboard-widget-medium,
|
||||||
|
.dashboard-widget-wide,
|
||||||
|
.dashboard-widget-full {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user