Document reporting interface patterns
This commit is contained in:
@@ -43,3 +43,7 @@ Operational and recovery behavior is documented in
|
|||||||
[docs/OPERATIONS.md](docs/OPERATIONS.md), while user and administrator tasks
|
[docs/OPERATIONS.md](docs/OPERATIONS.md), while user and administrator tasks
|
||||||
are covered by [docs/USER_GUIDE.md](docs/USER_GUIDE.md) and
|
are covered by [docs/USER_GUIDE.md](docs/USER_GUIDE.md) and
|
||||||
[docs/ADMIN_GUIDE.md](docs/ADMIN_GUIDE.md).
|
[docs/ADMIN_GUIDE.md](docs/ADMIN_GUIDE.md).
|
||||||
|
|
||||||
|
The Reporting route, workspace, state, consequence, and accessibility mapping
|
||||||
|
is recorded in
|
||||||
|
[docs/INTERFACE_PATTERN_MIGRATION.md](docs/INTERFACE_PATTERN_MIGRATION.md).
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Reporting Interface Pattern Migration
|
||||||
|
|
||||||
|
Reporting is a list-detail analytical workspace. It owns report semantics,
|
||||||
|
policy-aware result shaping, drill-through presentation, rendering, and
|
||||||
|
publication. Dataflow owns generic validation, typed expressions, schema
|
||||||
|
propagation, SQL compilation, and execution; Datasources and provider modules
|
||||||
|
own source access.
|
||||||
|
|
||||||
|
| Surface | Task and archetype | Consequence and state contract |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/reports` | Report catalogue and list-detail workspace | The catalogue keeps selected report context while parameters, results, history, and provenance change. `/reporting` is a compatibility route only. |
|
||||||
|
| Semantic report controls | Parameterized analytical query | Dimensions, measures, parameters, and pivot shape become a validated Dataflow-backed plan. Reporting does not execute unchecked presentation SQL. |
|
||||||
|
| Provider report controls | Governed cross-module report | Purpose, audience, permission, availability, privacy transforms, source revisions, and retention remain visible. A blocked Run action stays present with a keyboard-focusable reason. |
|
||||||
|
| Results, history, and export | Monitoring/reporting evidence | Loading, empty, failed, truncated, successful, historical, and provider-partial states remain distinguishable. Tables are the accessible fallback for visual results. |
|
||||||
|
| Save and schedule dialogs | Create/edit and asynchronous setup | Core dialogs retain focus and stable actions. Scheduling records a durable report definition/revision and does not imply immediate publication. |
|
||||||
|
|
||||||
|
Run, schedule, export, and publication are consequential actions. Backend
|
||||||
|
permissions remain authoritative; the WebUI mirrors them and explains disabled
|
||||||
|
actions without exposing protected rows. Optional Dataflow, Policy, Files,
|
||||||
|
Mail, Templates, Search, and Notifications integrations are capability-based.
|
||||||
|
The full-height three-region workspace collapses at narrow widths while keeping
|
||||||
|
the catalogue, result, and inspector in task order.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `npm run test:interface-pattern`
|
||||||
|
- Reporting service, provider-report, migration, manifest, and module-permutation tests
|
||||||
|
- the Core TypeScript graph, structural localization audit, theme check, module
|
||||||
|
permutations, and full-product bundle budget
|
||||||
@@ -455,6 +455,11 @@ manifest = ModuleManifest(
|
|||||||
href="govoplan-reporting/docs/ADMIN_GUIDE.md",
|
href="govoplan-reporting/docs/ADMIN_GUIDE.md",
|
||||||
kind="repository",
|
kind="repository",
|
||||||
),
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Reporting interface pattern audit",
|
||||||
|
href="govoplan-reporting/docs/INTERFACE_PATTERN_MIGRATION.md",
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
},
|
},
|
||||||
"./styles/reporting.css": "./src/styles/reporting.css"
|
"./styles/reporting.css": "./src/styles/reporting.css"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.14",
|
"@govoplan/core-webui": "^0.1.14",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const page = fs.readFileSync("src/features/reporting/ReportingPage.tsx", "utf8");
|
||||||
|
const provider = fs.readFileSync("src/features/reporting/ProviderReportWorkspace.tsx", "utf8");
|
||||||
|
const styles = fs.readFileSync("src/styles/reporting.css", "utf8");
|
||||||
|
|
||||||
|
assert.ok(page.includes("DocumentationHelpLink"), "Reporting exposes configured-system help");
|
||||||
|
assert.ok(page.includes("PageScrollViewport"), "Reporting owns bounded catalogue and inspector scrolling");
|
||||||
|
assert.ok(page.includes("DataGrid"), "Tabular report results use the shared grid");
|
||||||
|
assert.ok(page.includes("<Dialog"), "Save and schedule operations use shared dialogs");
|
||||||
|
assert.ok(provider.includes("disabledReason={runDisabledReason}"), "Governed report blockers remain keyboard-explainable");
|
||||||
|
assert.ok(provider.includes("DismissibleAlert"), "Provider failures and unavailable states use shared alerts");
|
||||||
|
assert.ok(!page.includes("window.alert("), "Reporting must not use browser alerts");
|
||||||
|
assert.ok(styles.includes("@media (max-width: 760px)"), "Reporting retains a narrow-viewport task order");
|
||||||
|
|
||||||
|
console.log("Reporting interface pattern contract passed.");
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
|
hasScope,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo
|
type AuthInfo
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
@@ -32,6 +33,7 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
|
|||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const tenant = auth.active_tenant ?? auth.tenant;
|
const tenant = auth.active_tenant ?? auth.tenant;
|
||||||
|
const canRun = hasScope(auth, "reporting:report:run");
|
||||||
const audienceScope = useMemo(() => ({
|
const audienceScope = useMemo(() => ({
|
||||||
scope_type: "tenant",
|
scope_type: "tenant",
|
||||||
scope_id: tenant.id,
|
scope_id: tenant.id,
|
||||||
@@ -101,6 +103,15 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
|
|||||||
const missingRequired = report.parameters.some((item) =>
|
const missingRequired = report.parameters.some((item) =>
|
||||||
item.required && (parameters[item.key] === undefined || parameters[item.key] === "")
|
item.required && (parameters[item.key] === undefined || parameters[item.key] === "")
|
||||||
);
|
);
|
||||||
|
const runDisabledReason = !canRun
|
||||||
|
? "Report run permission is required."
|
||||||
|
: !report.available
|
||||||
|
? report.unavailable_reason ?? "Policy does not allow this report."
|
||||||
|
: missingRequired
|
||||||
|
? "Complete the required report parameters."
|
||||||
|
: !purpose.trim()
|
||||||
|
? "Record the purpose for this governed report run."
|
||||||
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="reporting-result-header">
|
<header className="reporting-result-header">
|
||||||
@@ -113,7 +124,8 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void run()}
|
onClick={() => void run()}
|
||||||
disabled={!report.available || running || missingRequired || !purpose.trim()}>
|
disabled={running || Boolean(runDisabledReason)}
|
||||||
|
disabledReason={runDisabledReason}>
|
||||||
<Play size={16} aria-hidden="true" /> {running ? "Running" : "Run"}
|
<Play size={16} aria-hidden="true" /> {running ? "Running" : "Run"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
IconButton,
|
IconButton,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
@@ -202,6 +203,10 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<span className="reporting-count">{reports.length + providerReports.length} reports</span>
|
<span className="reporting-count">{reports.length + providerReports.length} reports</span>
|
||||||
|
<DocumentationHelpLink
|
||||||
|
reference={{ topicId: "reporting.governed-bi", documentationType: "user" }}
|
||||||
|
label="Open reporting documentation"
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Reload reports"
|
label="Reload reports"
|
||||||
icon={<RefreshCw size={17} />}
|
icon={<RefreshCw size={17} />}
|
||||||
@@ -265,7 +270,11 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
|
|||||||
<IconButton label="Schedule report" icon={<CalendarClock size={17} />} variant="ghost" onClick={() => setScheduleDialogOpen(true)} />
|
<IconButton label="Schedule report" icon={<CalendarClock size={17} />} variant="ghost" onClick={() => setScheduleDialogOpen(true)} />
|
||||||
}
|
}
|
||||||
<IconButton label="Save current view" icon={<Save size={17} />} variant="ghost" onClick={() => setSaveDialogOpen(true)} />
|
<IconButton label="Save current view" icon={<Save size={17} />} variant="ghost" onClick={() => setSaveDialogOpen(true)} />
|
||||||
<Button variant="primary" onClick={() => void execute()} disabled={!canRun || running}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => void execute()}
|
||||||
|
disabled={!canRun || running}
|
||||||
|
disabledReason={!canRun ? "Report run permission is required." : undefined}>
|
||||||
<Play size={16} aria-hidden="true" /> {running ? "Running" : "Run"}
|
<Play size={16} aria-hidden="true" /> {running ? "Running" : "Run"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user