Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b28229c003 | |||
| 2250d69765 | |||
| 64032b1ed1 | |||
| cfacc47ebb | |||
| 4a2d68b491 | |||
| a315a7c62b |
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: "Bug"
|
||||||
|
about: "Report a reproducible defect, regression, or incorrect behavior"
|
||||||
|
title: "[Bug] "
|
||||||
|
labels:
|
||||||
|
- type/bug
|
||||||
|
- status/triage
|
||||||
|
- module/dashboard
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Repository:
|
||||||
|
- Area/module:
|
||||||
|
- Affected version or commit:
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
Actual:
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Logs, screenshots, traces, or failing test output:
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Command or workflow that should pass when fixed:
|
||||||
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Docs / workflow"
|
||||||
|
about: "Request documentation, process, or developer workflow changes"
|
||||||
|
title: "[Docs] "
|
||||||
|
labels:
|
||||||
|
- type/docs
|
||||||
|
- status/triage
|
||||||
|
- module/dashboard
|
||||||
|
- area/docs
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Repository:
|
||||||
|
- Document or workflow:
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
What is missing, unclear, duplicated, or stale?
|
||||||
|
|
||||||
|
## Desired State
|
||||||
|
|
||||||
|
What should the docs or workflow make clear?
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
How should this be checked?
|
||||||
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: "Feature"
|
||||||
|
about: "Propose new user-visible behavior or platform capability"
|
||||||
|
title: "[Feature] "
|
||||||
|
labels:
|
||||||
|
- type/feature
|
||||||
|
- status/triage
|
||||||
|
- module/dashboard
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
What user, operator, or developer problem should this solve?
|
||||||
|
|
||||||
|
## Proposed Capability
|
||||||
|
|
||||||
|
What should exist when this is done?
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Owning repository:
|
||||||
|
- Related module repositories:
|
||||||
|
- Extension point or integration boundary:
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Command, scenario, or UI flow that should prove completion:
|
||||||
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: "Task"
|
||||||
|
about: "Track implementation, maintenance, or migration work"
|
||||||
|
title: "[Task] "
|
||||||
|
labels:
|
||||||
|
- type/task
|
||||||
|
- status/triage
|
||||||
|
- module/dashboard
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
What needs to be completed?
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Owning repository:
|
||||||
|
- In-scope:
|
||||||
|
- Out-of-scope:
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Command or manual check:
|
||||||
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: "Tech debt"
|
||||||
|
about: "Track cleanup, refactoring, risk reduction, or deferred engineering work"
|
||||||
|
title: "[Debt] "
|
||||||
|
labels:
|
||||||
|
- type/debt
|
||||||
|
- status/triage
|
||||||
|
- module/dashboard
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Cost
|
||||||
|
|
||||||
|
What does this make harder, riskier, slower, or more fragile?
|
||||||
|
|
||||||
|
## Desired Shape
|
||||||
|
|
||||||
|
What should the code, tests, or architecture look like afterwards?
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
What behavior, compatibility, or module boundary must be preserved?
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Focused checks that should pass:
|
||||||
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## Issue
|
||||||
|
|
||||||
|
Closes #
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Follow-up issues:
|
||||||
@@ -2,5 +2,4 @@
|
|||||||
|
|
||||||
__all__ = ["__version__"]
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
__version__ = "0.1.6"
|
__version__ = "0.1.8"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||||
from govoplan_core.core.modules import DocumentationTopic, FrontendModule, FrontendRoute, ModuleManifest, NavItem
|
from govoplan_core.core.modules import DocumentationTopic, FrontendModule, FrontendRoute, ModuleManifest, NavItem
|
||||||
|
from govoplan_core.core.views import ViewSurface
|
||||||
|
|
||||||
|
|
||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
@@ -16,6 +17,15 @@ manifest = ModuleManifest(
|
|||||||
package_name="@govoplan/dashboard-webui",
|
package_name="@govoplan/dashboard-webui",
|
||||||
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=(
|
||||||
|
ViewSurface(
|
||||||
|
id="dashboard.widget.installed-modules",
|
||||||
|
module_id="dashboard",
|
||||||
|
kind="section",
|
||||||
|
label="Installed modules widget",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
documentation=(
|
documentation=(
|
||||||
DocumentationTopic(
|
DocumentationTopic(
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshCw, RotateCcw, SlidersHorizontal } from "lucide-react";
|
import { RefreshCw, RotateCcw, SlidersHorizontal } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
AdminSelectionList,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
|
PageScrollViewport,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
dashboardWidgetsForModules,
|
dashboardWidgetsForModules,
|
||||||
hasAnyScope,
|
hasAnyScope,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
useEffectiveView,
|
||||||
usePlatformModules,
|
usePlatformModules,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo,
|
type AuthInfo,
|
||||||
@@ -22,7 +25,14 @@ type DashboardLayout = {
|
|||||||
|
|
||||||
export default function DashboardPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
export default function DashboardPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||||
const modules = usePlatformModules();
|
const modules = usePlatformModules();
|
||||||
const widgets = useMemo(() => dashboardWidgetsForModules(modules).filter((widget) => canUseWidget(auth, widget)), [auth, modules]);
|
const effectiveView = useEffectiveView();
|
||||||
|
const widgets = useMemo(
|
||||||
|
() =>
|
||||||
|
dashboardWidgetsForModules(modules, effectiveView).filter((widget) =>
|
||||||
|
canUseWidget(auth, widget)
|
||||||
|
),
|
||||||
|
[auth, effectiveView, modules]
|
||||||
|
);
|
||||||
const widgetSignature = widgets.map((widget) => widget.id).join("|");
|
const widgetSignature = widgets.map((widget) => widget.id).join("|");
|
||||||
const storageKey = `govoplan.dashboard.layout:${auth.active_tenant?.id ?? auth.tenant.id}:${auth.user.id}`;
|
const storageKey = `govoplan.dashboard.layout:${auth.active_tenant?.id ?? auth.tenant.id}:${auth.user.id}`;
|
||||||
const [layout, setLayout] = useState<DashboardLayout>(() => defaultLayout(widgets));
|
const [layout, setLayout] = useState<DashboardLayout>(() => defaultLayout(widgets));
|
||||||
@@ -50,14 +60,9 @@ export default function DashboardPage({ settings, auth }: { settings: ApiSetting
|
|||||||
writeLayout(storageKey, next);
|
writeLayout(storageKey, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWidgetVisible(widgetId: string, visible: boolean) {
|
function setVisibleWidgets(selected: string[]) {
|
||||||
const allIds = widgets.map((widget) => widget.id);
|
const allIds = widgets.map((widget) => widget.id);
|
||||||
const nextVisible = new Set(visibleWidgetIds);
|
const nextVisible = new Set(selected);
|
||||||
if (visible) {
|
|
||||||
nextVisible.add(widgetId);
|
|
||||||
} else {
|
|
||||||
nextVisible.delete(widgetId);
|
|
||||||
}
|
|
||||||
saveLayout({
|
saveLayout({
|
||||||
known: allIds,
|
known: allIds,
|
||||||
visible: allIds.filter((id) => nextVisible.has(id))
|
visible: allIds.filter((id) => nextVisible.has(id))
|
||||||
@@ -69,7 +74,8 @@ export default function DashboardPage({ settings, auth }: { settings: ApiSetting
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-pad workspace-data-page dashboard-page">
|
<PageScrollViewport className="dashboard-page">
|
||||||
|
<div className="content-pad workspace-data-page">
|
||||||
<div className="page-heading split workspace-heading">
|
<div className="page-heading split workspace-heading">
|
||||||
<div>
|
<div>
|
||||||
<PageTitle>i18n:govoplan-dashboard.dashboard.3f8b4df2</PageTitle>
|
<PageTitle>i18n:govoplan-dashboard.dashboard.3f8b4df2</PageTitle>
|
||||||
@@ -93,21 +99,11 @@ export default function DashboardPage({ settings, auth }: { settings: ApiSetting
|
|||||||
title="Dashboard widgets"
|
title="Dashboard widgets"
|
||||||
actions={<Button onClick={resetLayout}><RotateCcw size={16} /> Reset</Button>}>
|
actions={<Button onClick={resetLayout}><RotateCcw size={16} /> Reset</Button>}>
|
||||||
{widgets.length === 0 ? <p className="muted">No installed module exposes dashboard widgets yet.</p> :
|
{widgets.length === 0 ? <p className="muted">No installed module exposes dashboard widgets yet.</p> :
|
||||||
<div className="dashboard-widget-picker">
|
<AdminSelectionList
|
||||||
{widgets.map((widget) =>
|
options={widgets.map((widget) => ({ id: widget.id, label: widget.title, description: widget.description ?? widget.id }))}
|
||||||
<label key={widget.id} className="dashboard-widget-toggle">
|
selected={visibleWidgetIds}
|
||||||
<input
|
onChange={setVisibleWidgets}
|
||||||
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>
|
</Card>
|
||||||
}
|
}
|
||||||
@@ -133,7 +129,8 @@ export default function DashboardPage({ settings, auth }: { settings: ApiSetting
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>);
|
</div>
|
||||||
|
</PageScrollViewport>);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUseWidget(auth: AuthInfo, widget: DashboardWidgetContribution): boolean {
|
function canUseWidget(auth: AuthInfo, widget: DashboardWidgetContribution): boolean {
|
||||||
@@ -183,4 +180,3 @@ function moduleLabels(modules: Array<{ id: string }>): string {
|
|||||||
if (!modules.length) return "Core shell only";
|
if (!modules.length) return "Core shell only";
|
||||||
return modules.slice(0, 4).map((module) => module.id).join(", ");
|
return modules.slice(0, 4).map((module) => module.id).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const dashboardWidgets: DashboardWidgetsUiCapability = {
|
|||||||
widgets: [
|
widgets: [
|
||||||
{
|
{
|
||||||
id: "dashboard.installed-modules",
|
id: "dashboard.installed-modules",
|
||||||
|
surfaceId: "dashboard.widget.installed-modules",
|
||||||
title: "Installed modules",
|
title: "Installed modules",
|
||||||
description: "Enabled WebUI modules in this browser session.",
|
description: "Enabled WebUI modules in this browser session.",
|
||||||
moduleId: "dashboard",
|
moduleId: "dashboard",
|
||||||
@@ -33,6 +34,15 @@ export const dashboardModule: PlatformWebModule = {
|
|||||||
dependencies: ["access"],
|
dependencies: ["access"],
|
||||||
optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"],
|
optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"],
|
||||||
translations,
|
translations,
|
||||||
|
viewSurfaces: [
|
||||||
|
{
|
||||||
|
id: "dashboard.widget.installed-modules",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
kind: "section",
|
||||||
|
label: "Installed modules widget",
|
||||||
|
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, render: ({ settings, auth }) => createElement(DashboardPage, { settings, auth }) }
|
||||||
@@ -43,4 +53,3 @@ export const dashboardModule: PlatformWebModule = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default dashboardModule;
|
export default dashboardModule;
|
||||||
|
|
||||||
|
|||||||
@@ -32,39 +32,6 @@
|
|||||||
grid-column: 1 / -1;
|
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 {
|
.dashboard-compact-list.detail-list div {
|
||||||
grid-template-columns: minmax(92px, auto) minmax(0, 1fr);
|
grid-template-columns: minmax(92px, auto) minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@@ -82,7 +49,6 @@
|
|||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.dashboard-widget-grid,
|
.dashboard-widget-grid,
|
||||||
.dashboard-widget-picker,
|
|
||||||
.dashboard-widget-metrics.metric-grid {
|
.dashboard-widget-metrics.metric-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -94,4 +60,3 @@
|
|||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user