Skip to content

Commit 1f8fdd7

Browse files
committed
MDH: Overview reuses the shared empty-state card when there are no collections
The Overview panel showed a bare "No collections" line; it now renders the same CollectionEmptyState card ("No collections yet" + Create collection) as the collection view. The empty branch fires only when there are zero collections, so the component's "select a collection" branch is unreachable there. While disconnected the Overview now stays blank (the connection bar carries the reason) instead of showing "No collections".
1 parent e2e144a commit 1f8fdd7

4 files changed

Lines changed: 304 additions & 2 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Overview Empty-State Reuse Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Replace the Overview panel's bare `<div class="stats-empty">No collections</div>` with the shared `CollectionEmptyState` component, so an empty org shows the same "No collections yet" + Create-collection card as the collection view.
6+
7+
**Architecture:** `OverviewPanel` already renders its empty branch exactly when `collections.value.length === 0`, which is the condition `CollectionEmptyState` handles. Reuse the component directly, passing the `connected` signal's value.
8+
9+
**Tech Stack:** Preact + @preact/signals, Vitest (jsdom).
10+
11+
## Global Constraints
12+
13+
- Reuse `CollectionEmptyState` as-is; do NOT modify it.
14+
- The empty branch fires only when `totalCount === collections.value.length === 0`, so `CollectionEmptyState`'s `collections.length > 0` ("Select a collection") branch is unreachable here — correct by construction.
15+
- No new chrome.storage keys, no migration. The populated Overview (table, charts, sorting, totals) is untouched.
16+
- Leave the `.stats-empty` CSS rule in place (still used by `StatsPanel.jsx:180-181`).
17+
- No git commits in this run (owner rule) — stay on `master`, leave changes uncommitted; the final step is a test/build gate. The per-task diff is a working-tree diff scoped to the task's files.
18+
- Tests: `// @vitest-environment jsdom`, `h(Component, null)` + preact `render`, `vi.mock`.
19+
20+
---
21+
22+
### Task 1: Reuse `CollectionEmptyState` in the Overview
23+
24+
**Files:**
25+
- Modify: `src/mdh/components/OverviewPanel.jsx` (store import + component import + empty-branch swap)
26+
- Test: `tests/mdh-overview-empty.test.js` (new)
27+
28+
**Interfaces:**
29+
- Consumes: `CollectionEmptyState({ connected })` (default export of `src/mdh/components/CollectionEmptyState.jsx`, shipped in `ae66019`); the `connected` signal from `src/mdh/store.js`.
30+
- Produces: no new exports.
31+
32+
- [ ] **Step 1: Write the failing test**
33+
34+
Create `tests/mdh-overview-empty.test.js`. An empty Overview is inert (the initial-load effect calls `streamStats([])` → zero workers → no API calls; the live-poll effect early-returns on empty `cols`, so no timers/listeners; `OverviewPanel` uses no `chrome.*`). Mock `api.js` defensively and mock `Sidebar.jsx`'s `showCreateModal` (imported transitively by `CollectionEmptyState`), mirroring `tests/mdh-empty-state.test.js`.
35+
36+
```javascript
37+
// @vitest-environment jsdom
38+
import { describe, it, expect, beforeEach, vi } from 'vitest';
39+
import { h, render } from 'preact';
40+
41+
vi.mock('../src/mdh/api.js', () => ({ aggregate: vi.fn() }));
42+
vi.mock('../src/mdh/components/Sidebar.jsx', () => ({ showCreateModal: vi.fn() }));
43+
44+
import OverviewPanel from '../src/mdh/components/OverviewPanel.jsx';
45+
import { collections, connected, loading, error } from '../src/mdh/store.js';
46+
47+
function mount() {
48+
const root = document.createElement('div');
49+
render(h(OverviewPanel, null), root);
50+
return root;
51+
}
52+
53+
describe('OverviewPanel empty state', () => {
54+
beforeEach(() => {
55+
collections.value = [];
56+
connected.value = true;
57+
loading.value = false;
58+
error.value = null;
59+
});
60+
61+
it('reuses the shared no-collections empty state instead of bare text', () => {
62+
const root = mount();
63+
expect(root.textContent).toContain('No collections yet');
64+
expect(root.querySelector('button.btn-success')).toBeTruthy();
65+
// the old bare "No collections" .stats-empty div is gone
66+
expect(root.querySelector('.stats-empty')).toBeNull();
67+
// no stats table when empty
68+
expect(root.querySelector('table.stats-table')).toBeNull();
69+
});
70+
});
71+
```
72+
73+
- [ ] **Step 2: Run the test to verify it fails**
74+
75+
Run: `npx vitest run tests/mdh-overview-empty.test.js`
76+
Expected: FAIL — the panel still renders `<div class="stats-empty">No collections</div>`, so `.stats-empty` is present, "No collections yet" is absent, and there is no `button.btn-success`.
77+
78+
- [ ] **Step 3: Import the store signal and the component**
79+
80+
In `src/mdh/components/OverviewPanel.jsx`:
81+
82+
(a) Add `connected` to the store import (line 3), changing:
83+
84+
```jsx
85+
import { collections, selectedCollection, activeView } from '../store.js';
86+
```
87+
88+
to:
89+
90+
```jsx
91+
import { collections, selectedCollection, activeView, connected } from '../store.js';
92+
```
93+
94+
(b) Add the component import after the existing `OverviewCharts` import (line 8):
95+
96+
```jsx
97+
import CollectionEmptyState from './CollectionEmptyState.jsx';
98+
```
99+
100+
- [ ] **Step 4: Swap the empty branch**
101+
102+
In `src/mdh/components/OverviewPanel.jsx`, replace (lines 308-310):
103+
104+
```jsx
105+
{totalCount === 0 ? (
106+
<div class="stats-empty">No collections</div>
107+
) : (
108+
```
109+
110+
with:
111+
112+
```jsx
113+
{totalCount === 0 ? (
114+
<CollectionEmptyState connected={connected.value} />
115+
) : (
116+
```
117+
118+
(Leave the rest of the ternary — the `<table>` branch — unchanged.)
119+
120+
- [ ] **Step 5: Run the test to verify it passes**
121+
122+
Run: `npx vitest run tests/mdh-overview-empty.test.js`
123+
Expected: PASS (1 test).
124+
125+
- [ ] **Step 6: Run the full suite and rebuild**
126+
127+
Run: `npm test`
128+
Expected: all tests green (new file + no regressions).
129+
130+
Run: `npm run build`
131+
Expected: clean build into `dist/` (console.js emits with no errors).
132+
133+
- [ ] **Step 7: Manual verification + handoff (no commit)**
134+
135+
Reload the extension, open the Console → Dataset Management on an org with **no** collections, click **Overview**: it shows the "No collections yet" card + Create-collection button (was: bare "No collections"). On an org **with** collections the Overview table renders as before. Leave uncommitted on `master`.
136+
137+
---
138+
139+
## Self-Review
140+
141+
**Spec coverage:**
142+
- Import `connected` + `CollectionEmptyState`, swap the empty branch (spec §Design) → Task 1 Steps 3-4. ✓
143+
- Reuse component unchanged; unreachable "Select a collection" branch (spec §facts 1-2) → Task 1 Step 4 + Global Constraints. ✓
144+
- Layout fit via `.stats-scroll` flex column (spec §fact 3) → relied on, no code needed. ✓
145+
- Inert empty Overview / no chrome / no timers (spec §fact 4) → Task 1 Step 1 test rationale. ✓
146+
- `.stats-empty` left in place (spec §Design, still used by StatsPanel) → Global Constraints; no CSS change in the task. ✓
147+
- Test asserts card + button, absence of `.stats-empty` and table (spec §Testing) → Task 1 Step 1. ✓
148+
- Backward compat (no storage, populated Overview untouched) → Global Constraints; only the empty branch changes. ✓
149+
150+
**Placeholder scan:** No TBD/TODO; every code step shows complete code; every command has an expected result. ✓
151+
152+
**Type consistency:** `CollectionEmptyState({ connected })` consumed with `connected={connected.value}` (a boolean/null from the signal), matching its prop contract. Store signals `collections`/`connected`/`loading`/`error` match `src/mdh/store.js` exports. ✓
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Overview reuses the shared empty-state component
2+
3+
**Date:** 2026-07-09
4+
**Status:** Design approved, ready for implementation plan
5+
**Area:** `src/mdh/`
6+
7+
## Problem
8+
9+
The Dataset Management **Overview** panel shows a bare
10+
`<div class="stats-empty">No collections</div>` (`OverviewPanel.jsx:309`) when
11+
the org has no collections. The collection view already got a richer, actionable
12+
empty state (`CollectionEmptyState`, shipped in commit `ae66019`): a "No
13+
collections yet" card with a **Create collection** button. The Overview should
14+
reuse it for consistency instead of the plain text.
15+
16+
## Verified facts (grounding)
17+
18+
1. **Trigger is identical to the empty-org case.** In `OverviewPanel.jsx`,
19+
`totalCount = cols.length` and `cols = collections.value` (lines 48, 271), so
20+
the `totalCount === 0` branch (line 308-309) fires exactly when
21+
`collections.value.length === 0` — the same condition `CollectionEmptyState`
22+
treats as "no collections".
23+
2. **`CollectionEmptyState` is safe to reuse here.** Its first branch,
24+
`collections.value.length > 0 → "Select a collection to get started"`, is
25+
unreachable in the Overview because the Overview only mounts it when
26+
`totalCount === 0`. It then gates on `loading || !connected || error`
27+
`null`, else renders the "No collections yet" block.
28+
3. **Layout fits.** `.stats-scroll` is `display:flex; flex-direction:column;
29+
flex:1; min-height:0` (`console.css:2293-2296`). `CollectionEmptyState`
30+
renders `.empty-state` (`flex:1; display:flex; align-items:center;
31+
justify-content:center``console.css:330-333`), which grows to fill the
32+
scroll area and centers the card. When empty, the empty-state is the only
33+
child of `.stats-scroll` (the charts/progress track above are gated on
34+
`totalCount > 0`).
35+
4. **An empty Overview is fully inert (verified).** The initial-load effect
36+
(`OverviewPanel.jsx:158`) calls `streamStats([], …)`, whose
37+
`runWithConcurrency([], …)` spawns zero workers → no API calls. The live-poll
38+
effect early-returns at line 176 (`if (cols.length === 0) return`) → no
39+
`setTimeout`, no `visibilitychange` listener. `OverviewPanel` uses no
40+
`chrome.*` at all. So a test rendering it with empty `cols` needs no timers,
41+
no network, and no `chrome` stub for the panel itself.
42+
5. **`connected` is a store signal** (`store.js`), not currently imported by
43+
`OverviewPanel`. `CollectionEmptyState` takes `connected` as a prop and
44+
tolerates `null`/`false` (renders `null`).
45+
6. **No import cycle.** `OverviewPanel → CollectionEmptyState → Sidebar.jsx →
46+
(store, api, cache, …)`; `Sidebar` does not import `OverviewPanel`. Both are
47+
imported by `App.jsx`. No cycle.
48+
49+
## Design
50+
51+
In `src/mdh/components/OverviewPanel.jsx`:
52+
53+
- Add `connected` to the store import: `import { collections, selectedCollection,
54+
activeView, connected } from '../store.js';`
55+
- Add `import CollectionEmptyState from './CollectionEmptyState.jsx';`
56+
- Replace the empty branch:
57+
58+
```jsx
59+
{totalCount === 0 ? (
60+
<CollectionEmptyState connected={connected.value} />
61+
) : (
62+
<table class="stats-table stats-overview-table">
63+
...
64+
```
65+
66+
No other change. The `.stats-empty` CSS rule stays — it is still used by
67+
`StatsPanel.jsx:180-181` ("Discovering fields…" / "No fields found"), so it is
68+
not dead after this change.
69+
70+
### Behavior delta
71+
72+
- Empty + connected → "No collections yet" card + Create button (was: "No
73+
collections" text).
74+
- Empty + disconnected/errored → blank (was: "No collections"); the connection/
75+
error bars carry the reason — consistent with the collection view.
76+
77+
## Testing
78+
79+
New `tests/mdh-overview-empty.test.js` (jsdom, render via `h(OverviewPanel,
80+
null)`):
81+
82+
- With `collections.value = []`, `connected.value = true`: renders "No
83+
collections yet" and a `button.btn-success` ("Create collection"); does NOT
84+
render the bare "No collections" text and renders no stats `<table>`.
85+
- Mock `../src/mdh/api.js` defensively (no real network) and mock
86+
`../src/mdh/components/Sidebar.jsx` `showCreateModal` (imported transitively by
87+
`CollectionEmptyState`), following the pattern in
88+
`tests/mdh-empty-state.test.js`. No `chrome` stub or fake timers are needed
89+
(fact 4: the empty Overview spawns no workers, timers, or listeners).
90+
91+
## Backward compatibility
92+
93+
- No new storage keys; no migration.
94+
- Only the `totalCount === 0` render path in the Overview changes; the populated
95+
Overview (table, charts, sorting, totals) is untouched.
96+
- `CollectionEmptyState` itself is unchanged (reused as-is).
97+
- `.stats-empty` CSS rule left in place.
98+
99+
## Out of scope
100+
101+
- Any change to `CollectionEmptyState`.
102+
- `.stats-empty` CSS (still used by `StatsPanel`; untouched).
103+
- Charts / populated-Overview behavior.

src/mdh/components/OverviewPanel.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { h, Fragment } from 'preact';
22
import { useEffect, useState, useRef } from 'preact/hooks';
3-
import { collections, selectedCollection, activeView } from '../store.js';
3+
import { collections, selectedCollection, activeView, connected } from '../store.js';
44
import * as api from '../api.js';
55
import * as cache from '../cache.js';
66
import { buildStoragePipeline, buildBatchStoragePipeline } from '../statsPipelines.js';
77
import FlashOnChange from './FlashOnChange.jsx';
88
import OverviewCharts from './OverviewCharts.jsx';
9+
import CollectionEmptyState from './CollectionEmptyState.jsx';
910

1011
const BATCH_SIZE = 50;
1112
const BATCH_CONCURRENCY = 3;
@@ -306,7 +307,7 @@ export default function OverviewPanel() {
306307

307308
<div class="stats-scroll">
308309
{totalCount === 0 ? (
309-
<div class="stats-empty">No collections</div>
310+
<CollectionEmptyState connected={connected.value} />
310311
) : (
311312
<table class="stats-table stats-overview-table">
312313
<colgroup>

tests/mdh-overview-empty.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// @vitest-environment jsdom
2+
import { describe, it, expect, beforeEach, vi } from 'vitest';
3+
import { h, render } from 'preact';
4+
5+
vi.mock('../src/mdh/api.js', () => ({ aggregate: vi.fn() }));
6+
vi.mock('../src/mdh/components/Sidebar.jsx', () => ({ showCreateModal: vi.fn() }));
7+
8+
import OverviewPanel from '../src/mdh/components/OverviewPanel.jsx';
9+
import { collections, connected, loading, error } from '../src/mdh/store.js';
10+
11+
function mount() {
12+
const root = document.createElement('div');
13+
render(h(OverviewPanel, null), root);
14+
return root;
15+
}
16+
17+
describe('OverviewPanel empty state', () => {
18+
beforeEach(() => {
19+
collections.value = [];
20+
connected.value = true;
21+
loading.value = false;
22+
error.value = null;
23+
});
24+
25+
it('reuses the shared no-collections empty state instead of bare text', () => {
26+
const root = mount();
27+
expect(root.textContent).toContain('No collections yet');
28+
expect(root.querySelector('button.btn-success')).toBeTruthy();
29+
// the old bare "No collections" .stats-empty div is gone
30+
expect(root.querySelector('.stats-empty')).toBeNull();
31+
// no stats table when empty
32+
expect(root.querySelector('table.stats-table')).toBeNull();
33+
});
34+
35+
it('shows nothing (not the old "No collections") when empty and disconnected', () => {
36+
// CollectionEmptyState gates on !connected → renders null; the connection
37+
// bar carries the reason, matching the collection view. This is the behavior
38+
// delta from the old bare "No collections" text.
39+
connected.value = false;
40+
const root = mount();
41+
expect(root.textContent).not.toContain('No collections yet');
42+
expect(root.textContent).not.toContain('No collections');
43+
expect(root.querySelector('button.btn-success')).toBeNull();
44+
expect(root.querySelector('.stats-empty')).toBeNull();
45+
});
46+
});

0 commit comments

Comments
 (0)