Skip to content

Commit 0ad305f

Browse files
paddymulclaude
andcommitted
feat(spa): no-stats preview grid + elapsed timer while the buckaroo grid loads
While BuckarooDataPane waits on the WS session, fetch the first 200 rows from the data endpoint and render them in buckaroo's static DFViewer with summary_stats_data omitted, so the wait shows real rows instead of a bare spinner. An elapsed-seconds counter on the loading line makes a slow materialise read as progress, not a hang. Swaps to the live BuckarooEmbed once ws_url is ready; a preview miss (e.g. data endpoint 500) degrades silently to the spinner+timer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 61476f4 commit 0ad305f

6 files changed

Lines changed: 106 additions & 6 deletions

File tree

packages/app/src/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
EntryCache,
66
ActivityLog,
77
SessionResult,
8+
DataPage,
89
NotebookFull,
910
DiffData,
1011
ResultCache,
@@ -141,6 +142,9 @@ export const api = {
141142
session: (project: string, hash: string): Promise<SessionResult> =>
142143
get(`/${project}/api/session/${hash}`),
143144

145+
data: (project: string, hash: string, limit = 200): Promise<DataPage> =>
146+
get(`/${project}/api/data/${hash}?limit=${limit}`),
147+
144148
resultCache: (project: string): Promise<ResultCache> =>
145149
get(`/${project}/api/result_cache`),
146150

packages/app/src/components/BuckarooEmbed.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useEffect, useRef, useState } from "react";
2-
import { BuckarooServerView } from "buckaroo-js-core";
2+
import { BuckarooServerView, DFViewer } from "buckaroo-js-core";
33
import "buckaroo-js-core/style.css";
44
import { api } from "../api";
5+
import type { CellValue } from "../types";
6+
import { buildPreviewConfig, columnsFromRows } from "../previewGrid";
57

68
interface Props {
79
wsUrl: string;
@@ -22,6 +24,18 @@ export function BuckarooEmbed({ wsUrl, autoHeight, className = "buckaroo-embed",
2224
);
2325
}
2426

27+
/**
28+
* Static, no-summary-stats grid rendered straight from a page of rows (the data
29+
* endpoint), used as a fast preview while the live WS-backed session loads.
30+
* Omitting `summary_stats_data` is what makes it a no-stats view. Returns null
31+
* for an empty page so the caller can fall back to a plain spinner.
32+
*/
33+
export function PreviewGrid({ rows }: { rows: Record<string, CellValue>[] }) {
34+
const columns = columnsFromRows(rows);
35+
if (!columns.length) return null;
36+
return <DFViewer df_data={rows} df_viewer_config={buildPreviewConfig(columns)} />;
37+
}
38+
2539
interface LazyProps {
2640
hash: string;
2741
project: string;

packages/app/src/pages/CatalogPage.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { api } from "../api";
44
import { useSSE } from "../SSEContext";
55
import { recalcTarget } from "../recalcRefresh";
66
import { CatalogSidebar } from "../components/CatalogSidebar";
7-
import { BuckarooEmbed } from "../components/BuckarooEmbed";
7+
import { BuckarooEmbed, PreviewGrid } from "../components/BuckarooEmbed";
88
import { VegaChart } from "../components/VegaChart";
99
import { linkifyRefs } from "../codeLinks";
10-
import type { Entry, AppError, EntryDetail, EntryCache, SessionResult } from "../types";
10+
import type { Entry, AppError, EntryDetail, EntryCache, SessionResult, CellValue } from "../types";
1111

1212
// ── Error banner ──────────────────────────────────────────────────────────────
1313

@@ -353,11 +353,25 @@ function CodeWithLinks({
353353

354354
function BuckarooDataPane({ project, hash, totalRows }: { project: string; hash: string; totalRows: number }) {
355355
const [result, setResult] = useState<SessionResult | null>(null);
356+
const [preview, setPreview] = useState<Record<string, CellValue>[] | null>(null);
357+
const [elapsed, setElapsed] = useState(0);
356358
const [attempt, setAttempt] = useState(0);
357359

358360
useEffect(() => {
359361
let cancelled = false;
360362
setResult(null);
363+
setPreview(null);
364+
// Fill the wait with a quick, no-summary-stats peek at the first rows read
365+
// straight off the baked snapshot, while the live WS session spins up in
366+
// parallel. Best-effort: a preview miss just leaves the bare spinner.
367+
api
368+
.data(project, hash, 200)
369+
.then((d) => {
370+
if (!cancelled) setPreview(d.data);
371+
})
372+
.catch(() => {
373+
/* preview is optional */
374+
});
361375
api
362376
.session(project, hash)
363377
.then((d) => {
@@ -372,10 +386,30 @@ function BuckarooDataPane({ project, hash, totalRows }: { project: string; hash:
372386
};
373387
}, [project, hash, attempt]);
374388

389+
// Count up while the session loads so a slow materialise reads as progress,
390+
// not a hang. performance.now keeps it monotonic across wall-clock changes.
391+
useEffect(() => {
392+
if (result !== null) return;
393+
const start = performance.now();
394+
const id = window.setInterval(() => setElapsed((performance.now() - start) / 1000), 100);
395+
return () => window.clearInterval(id);
396+
}, [result, attempt]);
397+
375398
if (result === null) {
399+
const hasPreview = preview !== null && preview.length > 0;
376400
return (
377-
<div className="buckaroo-loading">
378-
<span className="spinner" /> loading data…
401+
<div className="buckaroo-loading-wrap">
402+
<div className="buckaroo-loading">
403+
<span className="spinner" /> loading data… {elapsed.toFixed(1)}s
404+
{hasPreview && (
405+
<span className="meta"> · preview, no summary stats — first {preview!.length} rows</span>
406+
)}
407+
</div>
408+
{hasPreview && (
409+
<div className="buckaroo-preview">
410+
<PreviewGrid rows={preview!} />
411+
</div>
412+
)}
379413
</div>
380414
);
381415
}

packages/app/src/previewGrid.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Minimal Buckaroo DFViewer config for the no-summary-stats preview grid shown
2+
// while the live (WS-backed) session spins up. Every column gets the generic
3+
// "obj" displayer: the preview is a quick peek at the first rows off the baked
4+
// snapshot, not the typed/formatted view with summary stats that the real
5+
// BuckarooServerView delivers once it connects.
6+
import type { CellValue } from "./types";
7+
8+
export interface PreviewColumnConfig {
9+
col_name: string;
10+
header_name: string;
11+
displayer_args: { displayer: "obj" };
12+
}
13+
14+
export interface PreviewViewerConfig {
15+
pinned_rows: never[];
16+
left_col_configs: never[];
17+
column_config: PreviewColumnConfig[];
18+
}
19+
20+
/** Column names from the first row, or [] when there are no rows. */
21+
export function columnsFromRows(rows: Record<string, CellValue>[]): string[] {
22+
return rows.length ? Object.keys(rows[0]) : [];
23+
}
24+
25+
export function buildPreviewConfig(columns: string[]): PreviewViewerConfig {
26+
return {
27+
pinned_rows: [],
28+
left_col_configs: [],
29+
column_config: columns.map((col_name) => ({
30+
col_name,
31+
header_name: col_name,
32+
displayer_args: { displayer: "obj" },
33+
})),
34+
};
35+
}

packages/app/src/styles.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,9 @@ table.cache-table .muted { color: var(--muted); }
538538
@keyframes tm-spin { to { transform: rotate(360deg); } }
539539
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: tm-spin 0.7s linear infinite; vertical-align: -2px; }
540540
.spinner-row { color: var(--muted); display: flex; align-items: center; gap: 8px; padding: 8px 0; }
541-
.buckaroo-loading { color: var(--muted); display: flex; align-items: center; gap: 8px; min-height: 120px; }
541+
.buckaroo-loading { color: var(--muted); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
542+
.buckaroo-loading-wrap { display: flex; flex-direction: column; gap: 8px; min-height: 120px; }
543+
.buckaroo-preview { width: 100%; height: 60vh; min-height: 240px; opacity: 0.72; }
542544
.buckaroo-error { background: #3a1e1e; border: 1px solid #7a3a3a; border-radius: 4px; padding: 12px; color: #ffd9d9; }
543545
.buckaroo-error .detail { color: #e8b4b8; margin: 4px 0; white-space: pre-wrap; }
544546
.buckaroo-error code { background: rgba(0, 0, 0, 0.25); padding: 1px 4px; border-radius: 3px; }

packages/app/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,17 @@ export interface SessionResult {
224224
detail: string;
225225
}
226226

227+
/** A JSON cell value as returned by the data endpoint. */
228+
export type CellValue = string | number | boolean | null | CellValue[] | { [key: string]: CellValue };
229+
230+
/** One page of an entry's rows, from GET /{project}/api/data/{hash}. */
231+
export interface DataPage {
232+
data: Record<string, CellValue>[];
233+
offset: number;
234+
limit: number;
235+
total: number;
236+
}
237+
227238
export interface LogEvent {
228239
ts: string;
229240
kind: "build_ok" | "build_error" | "alias_set" | "buckaroo" | string;

0 commit comments

Comments
 (0)