Document reporting interface patterns
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
},
|
||||
"./styles/reporting.css": "./src/styles/reporting.css"
|
||||
},
|
||||
"scripts": {
|
||||
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.14",
|
||||
"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,
|
||||
MetricCard,
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
@@ -32,6 +33,7 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const tenant = auth.active_tenant ?? auth.tenant;
|
||||
const canRun = hasScope(auth, "reporting:report:run");
|
||||
const audienceScope = useMemo(() => ({
|
||||
scope_type: "tenant",
|
||||
scope_id: tenant.id,
|
||||
@@ -101,6 +103,15 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
|
||||
const missingRequired = report.parameters.some((item) =>
|
||||
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 (
|
||||
<>
|
||||
<header className="reporting-result-header">
|
||||
@@ -113,7 +124,8 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
|
||||
<Button
|
||||
variant="primary"
|
||||
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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Button,
|
||||
DataGrid,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
IconButton,
|
||||
LoadingIndicator,
|
||||
@@ -202,6 +203,10 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
|
||||
/>
|
||||
</form>
|
||||
<span className="reporting-count">{reports.length + providerReports.length} reports</span>
|
||||
<DocumentationHelpLink
|
||||
reference={{ topicId: "reporting.governed-bi", documentationType: "user" }}
|
||||
label="Open reporting documentation"
|
||||
/>
|
||||
<IconButton
|
||||
label="Reload reports"
|
||||
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="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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user