fix: dashboard shows — while loading, exposes errors, adds refresh button
Some checks failed
Test / frontend-typecheck (push) Waiting to run
Test / frontend-tests (push) Waiting to run
Auto Tag / auto-tag (push) Successful in 4s
Test / rust-fmt-check (push) Successful in 1m7s
Release / build-macos-arm64 (push) Successful in 4m1s
Test / rust-clippy (push) Successful in 7m26s
Test / rust-tests (push) Has been cancelled
Release / build-windows-amd64 (push) Has been cancelled
Release / build-linux-arm64 (push) Has been cancelled
Release / build-linux-amd64 (push) Has been cancelled

Stat cards showed 0 immediately on mount because openCount was computed
from an empty issues array before the async loadIssues call resolved.
Now shows — during load, a red error banner if the backend call fails,
and a Refresh button. useEffect dependency changed to [] (fires once on
mount, not on every loadIssues reference check).
This commit is contained in:
Shaun Arman 2026-03-31 08:55:05 -05:00
parent 5537b0b042
commit 944b14e5c4
2 changed files with 37 additions and 13 deletions

View File

@ -1,16 +1,16 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Plus, AlertTriangle, CheckCircle, Clock } from "lucide-react"; import { Plus, AlertTriangle, CheckCircle, Clock, RefreshCw } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent, Badge, Button } from "@/components/ui"; import { Card, CardHeader, CardTitle, CardContent, Badge, Button } from "@/components/ui";
import { useHistoryStore } from "@/stores/historyStore"; import { useHistoryStore } from "@/stores/historyStore";
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const { issues, loadIssues, isLoading } = useHistoryStore(); const { issues, loadIssues, isLoading, error } = useHistoryStore();
useEffect(() => { useEffect(() => {
loadIssues(); loadIssues();
}, [loadIssues]); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const openCount = issues.filter((i) => i.status === "open" || i.status === "triaging").length; const openCount = issues.filter((i) => i.status === "open" || i.status === "triaging").length;
const resolvedThisWeek = issues.filter((i) => { const resolvedThisWeek = issues.filter((i) => {
@ -25,6 +25,7 @@ export default function Dashboard() {
).length; ).length;
const recentIssues = issues.slice(0, 10); const recentIssues = issues.slice(0, 10);
const statValue = (n: number) => (isLoading ? "—" : String(n));
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
@ -36,12 +37,24 @@ export default function Dashboard() {
IT Triage & Root Cause Analysis IT Triage & Root Cause Analysis
</p> </p>
</div> </div>
<Button onClick={() => navigate("/new-issue")}> <div className="flex items-center gap-2">
<Plus className="w-4 h-4 mr-2" /> <Button variant="outline" size="sm" onClick={() => loadIssues()} disabled={isLoading}>
New Issue <RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
</Button> Refresh
</Button>
<Button onClick={() => navigate("/new-issue")}>
<Plus className="w-4 h-4 mr-2" />
New Issue
</Button>
</div>
</div> </div>
{error && (
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
Failed to load issues: {error}
</div>
)}
{/* Stat cards */} {/* Stat cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card> <Card>
@ -50,7 +63,7 @@ export default function Dashboard() {
<Clock className="w-4 h-4 text-muted-foreground" /> <Clock className="w-4 h-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{openCount}</div> <div className="text-2xl font-bold">{statValue(openCount)}</div>
<p className="text-xs text-muted-foreground">Currently active</p> <p className="text-xs text-muted-foreground">Currently active</p>
</CardContent> </CardContent>
</Card> </Card>
@ -60,7 +73,7 @@ export default function Dashboard() {
<CheckCircle className="w-4 h-4 text-green-600" /> <CheckCircle className="w-4 h-4 text-green-600" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{resolvedThisWeek}</div> <div className="text-2xl font-bold">{statValue(resolvedThisWeek)}</div>
<p className="text-xs text-muted-foreground">Last 7 days</p> <p className="text-xs text-muted-foreground">Last 7 days</p>
</CardContent> </CardContent>
</Card> </Card>
@ -70,7 +83,7 @@ export default function Dashboard() {
<AlertTriangle className="w-4 h-4 text-destructive" /> <AlertTriangle className="w-4 h-4 text-destructive" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{criticalCount}</div> <div className="text-2xl font-bold">{statValue(criticalCount)}</div>
<p className="text-xs text-muted-foreground">Require immediate attention</p> <p className="text-xs text-muted-foreground">Require immediate attention</p>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -34,9 +34,7 @@ describe("History Store", () => {
expect(useHistoryStore.getState().isLoading).toBe(false); expect(useHistoryStore.getState().isLoading).toBe(false);
}); });
it("loadIssues sets error on failure without clearing existing issues", async () => { it("loadIssues sets error on failure and clears isLoading", async () => {
const existing = [makeIssue()];
useHistoryStore.setState({ issues: existing });
mockInvoke.mockRejectedValueOnce(new Error("DB locked")); mockInvoke.mockRejectedValueOnce(new Error("DB locked"));
await useHistoryStore.getState().loadIssues(); await useHistoryStore.getState().loadIssues();
@ -45,6 +43,19 @@ describe("History Store", () => {
expect(useHistoryStore.getState().isLoading).toBe(false); expect(useHistoryStore.getState().isLoading).toBe(false);
}); });
it("isLoading is true while fetching (stat cards must show — not 0)", async () => {
let resolve!: (v: unknown) => void;
mockInvoke.mockReturnValueOnce(new Promise((r) => (resolve = r)));
const p = useHistoryStore.getState().loadIssues();
expect(useHistoryStore.getState().isLoading).toBe(true);
resolve([makeIssue()]);
await p;
expect(useHistoryStore.getState().isLoading).toBe(false);
expect(useHistoryStore.getState().issues).toHaveLength(1);
});
it("open issue count includes status=open and status=triaging", () => { it("open issue count includes status=open and status=triaging", () => {
useHistoryStore.setState({ useHistoryStore.setState({
issues: [ issues: [