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 { 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 { useHistoryStore } from "@/stores/historyStore";
export default function Dashboard() {
const navigate = useNavigate();
const { issues, loadIssues, isLoading } = useHistoryStore();
const { issues, loadIssues, isLoading, error } = useHistoryStore();
useEffect(() => {
loadIssues();
}, [loadIssues]);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const openCount = issues.filter((i) => i.status === "open" || i.status === "triaging").length;
const resolvedThisWeek = issues.filter((i) => {
@ -25,6 +25,7 @@ export default function Dashboard() {
).length;
const recentIssues = issues.slice(0, 10);
const statValue = (n: number) => (isLoading ? "—" : String(n));
return (
<div className="p-6 space-y-6">
@ -36,12 +37,24 @@ export default function Dashboard() {
IT Triage & Root Cause Analysis
</p>
</div>
<Button onClick={() => navigate("/new-issue")}>
<Plus className="w-4 h-4 mr-2" />
New Issue
</Button>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => loadIssues()} disabled={isLoading}>
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
Refresh
</Button>
<Button onClick={() => navigate("/new-issue")}>
<Plus className="w-4 h-4 mr-2" />
New Issue
</Button>
</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 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
@ -50,7 +63,7 @@ export default function Dashboard() {
<Clock className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<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>
</CardContent>
</Card>
@ -60,7 +73,7 @@ export default function Dashboard() {
<CheckCircle className="w-4 h-4 text-green-600" />
</CardHeader>
<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>
</CardContent>
</Card>
@ -70,7 +83,7 @@ export default function Dashboard() {
<AlertTriangle className="w-4 h-4 text-destructive" />
</CardHeader>
<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>
</CardContent>
</Card>

View File

@ -34,9 +34,7 @@ describe("History Store", () => {
expect(useHistoryStore.getState().isLoading).toBe(false);
});
it("loadIssues sets error on failure without clearing existing issues", async () => {
const existing = [makeIssue()];
useHistoryStore.setState({ issues: existing });
it("loadIssues sets error on failure and clears isLoading", async () => {
mockInvoke.mockRejectedValueOnce(new Error("DB locked"));
await useHistoryStore.getState().loadIssues();
@ -45,6 +43,19 @@ describe("History Store", () => {
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", () => {
useHistoryStore.setState({
issues: [