Skip to content

Commit 5e44206

Browse files
author
sqlopt
committed
feat(ux): Pass 5 — close interaction loops (in-workspace fetch, verify, execution-free validate)
1 parent 0e0d0e0 commit 5e44206

13 files changed

Lines changed: 1367 additions & 52 deletions

web/src/App.tsx

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ const NAV_SECTIONS: { group: NavGroup; items: typeof WORKSPACES }[] = (
7373
["START", "OPERATE", "INSPECT", "SETUP"] as NavGroup[]
7474
).map((group) => ({ group, items: WORKSPACES.filter((w) => w.group === group) }));
7575

76+
// Pass 5 A1: the chart workspaces whose empty state pulls a live DMV bundle in
77+
// place (vs. routing to CONN). Entering one with a connection + null dmv triggers
78+
// an auto-pull. PLAN/SEV are SQL/plan-driven, not DMV-driven, so they're absent.
79+
const DMV_CHART_WORKSPACES: Workspace[] = ["indexes", "sizes"];
80+
7681
export function App() {
7782
// ── Persistent state ────────────────────────────────
7883
const [ui, setUi] = useState<P.UiPrefs>(() => ({
@@ -114,6 +119,13 @@ export function App() {
114119

115120
// ── Runtime state ───────────────────────────────────
116121
const [dmv, setDmv] = useState<unknown>(null);
122+
// Pass 5 A1: in-workspace DMV pull. The chart workspaces (PLAN/INDEX/SIZE/SEV)
123+
// used to bounce the user to CONN; instead they pull the DMV bundle IN PLACE.
124+
// `dmvLoading` drives the empty-state "Pulling DMVs…" spinner; `dmvErr` shows
125+
// an inline retry (NOT a redirect). pullDmvInline() sets the bundle and the
126+
// existing analyzer effect (keyed on `dmv`) regenerates report.charts.
127+
const [dmvLoading, setDmvLoading] = useState(false);
128+
const [dmvErr, setDmvErr] = useState<string | null>(null);
117129
const [report, setReport] = useState<AnalysisReport | null>(null);
118130
const [analyzing, setAnalyzing] = useState(false);
119131
const [backendOk, setBackendOk] = useState<boolean | null>(null);
@@ -152,6 +164,15 @@ export function App() {
152164
P.save("conn", toStore);
153165
}, [conn]);
154166

167+
// A new server·db is a new DMV scope: drop any prior bundle + inline error so
168+
// a chart never shows another server's telemetry and the auto-pull can re-fire.
169+
useEffect(() => {
170+
setDmv(null);
171+
setDmvErr(null);
172+
// Only on identity change of the active server/database.
173+
// eslint-disable-next-line react-hooks/exhaustive-deps
174+
}, [conn.server, conn.database]);
175+
155176
// Reflect edits to the active connection back into its saved profile, and
156177
// persist the profile list. Keeps the active server in sync as it's tweaked.
157178
useEffect(() => {
@@ -284,6 +305,51 @@ export function App() {
284305
reader.readAsText(file);
285306
}
286307

308+
// ── In-workspace DMV pull (Pass 5 A1) ───────────────
309+
// Pull the DMV bundle IN PLACE from a chart workspace. Mirrors the CONN-path
310+
// pull but keeps the user where they are: on success setDmv(bundle) → the
311+
// analyzer effect (keyed on dmv) rebuilds report.charts; on failure we surface
312+
// an inline error so the chart's empty state can offer Retry. Returns nothing;
313+
// callers just trigger it. A live `dmvLoading` guard prevents double-pulls.
314+
async function pullDmvInline() {
315+
if (!conn.server || dmvLoading) return;
316+
setDmvLoading(true);
317+
setDmvErr(null);
318+
try {
319+
const info = {
320+
server: conn.server,
321+
database: conn.database || undefined,
322+
user: conn.auth_mode === "sql" ? conn.user : undefined,
323+
password: conn.auth_mode === "sql" ? conn.password : undefined,
324+
trust_cert: conn.trust_cert,
325+
};
326+
const bundle = await backend.pullDmv(info as any);
327+
setDmv(bundle);
328+
} catch (e: any) {
329+
setDmvErr(e?.message ?? String(e));
330+
} finally {
331+
setDmvLoading(false);
332+
}
333+
}
334+
335+
// Auto-pull on first entry to a chart workspace when connected and dmv is null.
336+
// Keyed on the workspace + connection + whether dmv exists; the dmvLoading /
337+
// dmvErr guards (inside pullDmvInline and here) stop it from looping or
338+
// hammering after a failure (the user retries explicitly via the inline button).
339+
useEffect(() => {
340+
if (
341+
DMV_CHART_WORKSPACES.includes(ui.workspace) &&
342+
!!conn.server &&
343+
dmv == null &&
344+
!dmvLoading &&
345+
!dmvErr
346+
) {
347+
void pullDmvInline();
348+
}
349+
// pullDmvInline is stable enough for this guard set; deps are the real inputs.
350+
// eslint-disable-next-line react-hooks/exhaustive-deps
351+
}, [ui.workspace, conn.server, dmv, dmvLoading, dmvErr]);
352+
287353
const [explainBusy, setExplainBusy] = useState(false);
288354
const [explainErr, setExplainErr] = useState<string | null>(null);
289355

@@ -365,7 +431,13 @@ export function App() {
365431
</span>
366432
)}
367433
</div>
368-
{dmv ? (
434+
{dmvLoading ? (
435+
<div className="group">
436+
<span className="dot busy" />
437+
<span className="k">dmv</span>
438+
<span className="v">PULLING…</span>
439+
</div>
440+
) : dmv ? (
369441
<div className="group">
370442
<span className="dot ok" />
371443
<span className="k">dmv</span>
@@ -515,6 +587,8 @@ export function App() {
515587
data={report?.charts.plan_treemap ?? []}
516588
theme={ui.theme}
517589
action={{ label: "Generate from SQL", onClick: () => setUi({ ...ui, workspace: "analyze" }) }}
590+
loading={dmvLoading}
591+
error={dmvErr}
518592
/>
519593
</ChartContainer>
520594
</Workspace>
@@ -526,7 +600,13 @@ export function App() {
526600
<IndexHeatmap
527601
data={report?.charts.index_heatmap ?? []}
528602
theme={ui.theme}
529-
action={{ label: "Connect & pull DMVs", onClick: () => setUi({ ...ui, workspace: "connection" }) }}
603+
action={
604+
conn.server
605+
? { label: "Pull now", onClick: () => void pullDmvInline() }
606+
: { label: "Connect & pull DMVs", onClick: () => setUi({ ...ui, workspace: "connection" }) }
607+
}
608+
loading={dmvLoading}
609+
error={dmvErr}
530610
/>
531611
</ChartContainer>
532612
</Workspace>
@@ -538,7 +618,13 @@ export function App() {
538618
<SizeTreemap
539619
data={report?.charts.size_treemap ?? []}
540620
theme={ui.theme}
541-
action={{ label: "Connect & pull DMVs", onClick: () => setUi({ ...ui, workspace: "connection" }) }}
621+
action={
622+
conn.server
623+
? { label: "Pull now", onClick: () => void pullDmvInline() }
624+
: { label: "Connect & pull DMVs", onClick: () => setUi({ ...ui, workspace: "connection" }) }
625+
}
626+
loading={dmvLoading}
627+
error={dmvErr}
542628
/>
543629
</ChartContainer>
544630
</Workspace>
@@ -551,6 +637,8 @@ export function App() {
551637
data={report?.charts.severity_timeline ?? []}
552638
theme={ui.theme}
553639
action={{ label: "Paste T-SQL to analyze", onClick: () => setUi({ ...ui, workspace: "analyze" }) }}
640+
loading={dmvLoading}
641+
error={dmvErr}
554642
/>
555643
</ChartContainer>
556644
</Workspace>

web/src/components/EmptyChart.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,55 @@
22
* Empty-state card for charts and panels. Optionally renders a single
33
* primary CTA so an empty chart is never a dead end — the caller passes
44
* `action` to route the user somewhere useful (connect, load a plan, …).
5+
*
6+
* Pass 5 A1: a chart can now PULL its own DMV bundle in place rather than
7+
* routing the user back to CONN. `loading` swaps the card into a "Pulling
8+
* DMVs…" spinner state; `error` shows an inline failure with the same
9+
* `action` button re-labelled "Retry" (no redirect). The action itself does
10+
* the in-place pull (App.pullDmvInline), so the chart fills without leaving.
511
*/
612
export function EmptyChart({
713
glyph,
814
title,
915
hint,
1016
action,
17+
loading,
18+
error,
1119
}: {
1220
glyph: string;
1321
title: string;
1422
hint: string;
1523
action?: { label: string; onClick: () => void };
24+
/** True while the in-place DMV pull is in flight → shows the spinner state. */
25+
loading?: boolean;
26+
/** Inline error from the last pull attempt — keeps the user in place to retry. */
27+
error?: string | null;
1628
}) {
29+
if (loading) {
30+
return (
31+
<div className="empty">
32+
<div className="empty-card empty-loading">
33+
<div className="empty-spinner" aria-hidden />
34+
<div className="empty-title">Pulling DMVs…</div>
35+
<div className="empty-hint">
36+
Reading live performance views from the connected server. This can take a few seconds.
37+
</div>
38+
</div>
39+
</div>
40+
);
41+
}
42+
1743
return (
1844
<div className="empty">
1945
<div className="empty-card">
2046
<div className="empty-glyph">{glyph}</div>
2147
<div className="empty-title">{title}</div>
2248
<div className="empty-hint">{hint}</div>
49+
{error && <div className="empty-error">{error}</div>}
2350
{action && (
2451
<div className="empty-action">
2552
<button className="btn primary" onClick={action.onClick}>
26-
{action.label}
53+
{error ? "Retry" : action.label}
2754
</button>
2855
</div>
2956
)}

0 commit comments

Comments
 (0)