Skip to content

Commit f6b2cc5

Browse files
committed
MDH: trim record-list footer to "Showing X-Y"; slow-query warning moves to the Pipeline Debug
The total collection count and query timing are already visible in the Aggregate Pipeline Debug (the input-row $collStats count and per-stage timings), so the footer now shows only "Showing X-Y". The >1s slow-query tint (record-count-slow) is dropped from the footer and re-homed on the debug's per-stage and input timings, which now turn orange (var(--warning)) once a prefix takes over 1s.
1 parent 4ab0ed5 commit f6b2cc5

5 files changed

Lines changed: 117 additions & 11 deletions

File tree

src/console/console.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@ body {
499499
cursor: help;
500500
}
501501

502+
/* Slow query (>1s end-to-end for this prefix): warn in orange. Full opacity so
503+
it stands out from the muted regular timings. */
504+
.pipeline-debug-time-slow { color: var(--warning); opacity: 1; font-weight: 600; }
505+
502506
.pipeline-debug-total {
503507
opacity: 0.6; cursor: default;
504508
padding-left: 22px;
@@ -1082,8 +1086,6 @@ body {
10821086
color: var(--text-secondary); opacity: 0.4; cursor: default;
10831087
}
10841088

1085-
.record-count-slow { color: var(--warning); }
1086-
10871089
.split-pane-label { font-size: 11px; color: var(--text-secondary); }
10881090

10891091
.pipeline-header {

src/mdh/components/PipelineDebug.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import * as api from '../api.js';
66

77
const DEBUG_PREVIEW_LIMIT = 5;
88

9+
// A stage/input whose measured end-to-end latency exceeds this is flagged slow
10+
// (the timing turns orange). Matches the threshold the record-list footer used
11+
// before the slow-query warning moved here from the footer.
12+
const SLOW_QUERY_MS = 1000;
13+
const timeCls = (ms) => 'pipeline-debug-time' + (ms > SLOW_QUERY_MS ? ' pipeline-debug-time-slow' : '');
14+
915
// Map a stage/input count result ({count} | {error} | undefined) to the count
1016
// cell's text + class. Shared by the per-stage rows and the 0th input row.
1117
function countCell(info) {
@@ -137,7 +143,7 @@ export default function PipelineDebug({ entries, onToggleStage }) {
137143
<span class="pipeline-debug-preview">all records (pipeline input)</span>
138144
<span class="pipeline-debug-arrow">{'→'}</span>
139145
<span class={inputCell.cls}>{inputCell.text}</span>
140-
{inputInfo?.ms != null && (<span class="pipeline-debug-time" title={inputTimingTitle}>{inputInfo.ms}ms</span>)}
146+
{inputInfo?.ms != null && (<span class={timeCls(inputInfo.ms)} title={inputTimingTitle}>{inputInfo.ms}ms</span>)}
141147
</div>
142148
{inputInfo?.error && (
143149
<div class="pipeline-debug-error-detail" onClick={(e) => e.stopPropagation()}>
@@ -193,7 +199,7 @@ export default function PipelineDebug({ entries, onToggleStage }) {
193199
<span class="pipeline-debug-preview">{preview}</span>
194200
<span class="pipeline-debug-arrow">{'→'}</span>
195201
<span class={countCls}>{countText}</span>
196-
{info?.ms != null && (<span class="pipeline-debug-time" title={timingTitle}>{info.ms}ms</span>)}
202+
{info?.ms != null && (<span class={timeCls(info.ms)} title={timingTitle}>{info.ms}ms</span>)}
197203
</div>
198204
</StageTooltip>
199205
{info?.error && (

src/mdh/components/RecordList.jsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { h } from 'preact';
22
import { useState, useEffect, useRef } from 'preact/hooks';
3-
import { skip, limit, selectedCollection, selectionMode, selectedIds, selectionPipelineDirty } from '../store.js';
3+
import { skip, selectedCollection, selectionMode, selectedIds, selectionPipelineDirty } from '../store.js';
44
import RecordCard from './RecordCard.jsx';
55
import DownloadSplitButton from './DownloadSplitButton.jsx';
66
import JSON5 from 'json5';
@@ -120,10 +120,11 @@ export default function RecordList({
120120
}
121121

122122
const s = skip.value;
123-
const l = limit.value;
124-
let countText = records.length > 0 ? `Showing ${s + 1}\u2013${s + records.length}` : 'No records';
125-
if (totalCount !== null) countText += ` of ${totalCount.toLocaleString()} in collection (unfiltered)`;
126-
if (lastQueryMs) countText += ` \u00b7 ${lastQueryMs}ms`;
123+
// The footer intentionally shows only "Showing X-Y". The total collection count
124+
// and the query timing (plus the >1s slow-query warning) now live in the Aggregate
125+
// Pipeline Debug, so `totalCount` / `lastQueryMs` are accepted (DataPanel still
126+
// passes them) but no longer rendered here.
127+
const countText = records.length > 0 ? `Showing ${s + 1}\u2013${s + records.length}` : 'No records';
127128

128129
return (
129130
<div style="display:flex;flex-direction:column;flex:1;overflow:hidden">
@@ -179,8 +180,8 @@ export default function RecordList({
179180
))}
180181
</div>
181182
<div class="pagination">
182-
<span class={'record-count' + (lastQueryMs > 1000 ? ' record-count-slow' : '')} title="Total is the unfiltered collection size — it does not reflect the active pipeline filters">{countText}</span>
183-
<span class="pagination-hint">Click key to sort {'\u00b7'} Click value to filter {'\u00b7'} {ALT_KEY}+click or hover to copy</span>
183+
<span class="record-count">{countText}</span>
184+
<span class="pagination-hint">Click key to sort {'\u00b7'} Click value to filter {'\u00b7'} {ALT_KEY}+click to copy</span>
184185
<div class="pagination-controls">
185186
<button disabled={!pagination.hasPrev()} onClick={() => onPageChange('prev')}>{'\u2190'} Prev</button>
186187
<span>Page {pagination.page()}</span>

tests/mdh-pipeline-debug.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,34 @@ describe('PipelineDebug', () => {
265265
const inputTime = root.querySelector('.pipeline-debug-input-row .pipeline-debug-time');
266266
expect(inputTime).not.toBeNull();
267267
expect(inputTime.textContent).toMatch(/^\d+ms$/);
268+
269+
// Fast (sub-second) timings must NOT be flagged slow.
270+
expect(root.querySelector('.pipeline-debug-time-slow')).toBeNull();
271+
});
272+
273+
it('flags a stage timing as slow (orange) when its latency exceeds 1s', async () => {
274+
const pipeline = [{ $match: {} }];
275+
api.aggregate.mockResolvedValue({ result: [{ n: 1 }] });
276+
277+
// Force a measured latency well over the 1s threshold by advancing the clock
278+
// by 5s on every read (t0 vs. resolve → a multi-second delta), deterministically.
279+
const realNow = performance.now.bind(performance);
280+
let t = 0;
281+
performance.now = () => { t += 5000; return t; };
282+
try {
283+
const root = mount({ pipeline });
284+
await waitFor(
285+
() => root.querySelector('.pipeline-debug-row:not(.pipeline-debug-input-row) .pipeline-debug-time-slow'),
286+
'a slow-flagged stage timing to render',
287+
);
288+
const slow = root.querySelector('.pipeline-debug-row:not(.pipeline-debug-input-row) .pipeline-debug-time-slow');
289+
expect(slow).not.toBeNull();
290+
// It is still the timing element, just additionally flagged.
291+
expect(slow.classList.contains('pipeline-debug-time')).toBe(true);
292+
expect(slow.textContent).toMatch(/^\d+ms$/);
293+
} finally {
294+
performance.now = realNow;
295+
}
268296
});
269297

270298
it('renders timing on error rows too (request still took time)', async () => {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// @vitest-environment jsdom
2+
//
3+
// The record-list footer was decluttered to show ONLY "Showing X–Y": the total
4+
// count ("of N in collection (unfiltered)") and the query timing ("· Nms") now
5+
// live in the Aggregate Pipeline Debug, so they were removed here — along with the
6+
// >1s slow-query tint (record-count-slow), which moved to the debug timings.
7+
//
8+
import { describe, it, expect, beforeEach, vi } from 'vitest';
9+
import { h, render } from 'preact';
10+
11+
globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} };
12+
globalThis.chrome = { storage: { local: { get: (k, cb) => cb && cb({}), set() {}, remove() {} } } };
13+
14+
vi.mock('../src/mdh/api.js');
15+
vi.mock('../src/mdh/components/RecordCard.jsx', () => ({ default: () => h('div', { class: 'record-card-stub' }) }));
16+
vi.mock('../src/mdh/components/DownloadSplitButton.jsx', () => ({ default: () => h('div') }));
17+
18+
import RecordList from '../src/mdh/components/RecordList.jsx';
19+
import { skip, limit, selectedCollection, selectionMode, selectedIds, selectionPipelineDirty } from '../src/mdh/store.js';
20+
21+
const pagination = { hasPrev: () => false, hasNext: () => false, page: () => 1 };
22+
23+
function renderList(props = {}) {
24+
const root = document.createElement('div');
25+
document.body.appendChild(root);
26+
render(h(RecordList, {
27+
records: [{ _id: '1' }, { _id: '2' }],
28+
pipelineText: '[]', filterState: {}, sortState: {},
29+
lastQueryMs: 0, totalCount: null, pagination,
30+
onSort() {}, onFilter() {}, onPageChange() {}, onEdit() {}, onDelete() {}, onRefresh() {},
31+
downloadState: null, onCancelDownload() {}, onEnterSelectionMode() {}, onExitSelectionMode() {},
32+
onBulkDelete() {}, onBulkUpdate() {}, onSelectPage() {}, onClearSelection() {}, onViewSelected() {},
33+
...props,
34+
}), root);
35+
return root;
36+
}
37+
38+
beforeEach(() => {
39+
vi.clearAllMocks();
40+
skip.value = 0;
41+
limit.value = 50;
42+
selectedCollection.value = null;
43+
selectionMode.value = false;
44+
selectedIds.value = new Map();
45+
selectionPipelineDirty.value = false;
46+
});
47+
48+
describe('RecordList footer', () => {
49+
it('shows only "Showing X–Y" — no total count, no timing', () => {
50+
const root = renderList({ totalCount: 162, lastQueryMs: 277 });
51+
const count = root.querySelector('.record-count');
52+
expect(count).not.toBeNull();
53+
expect(count.textContent).toBe('Showing 1–2');
54+
expect(count.textContent).not.toContain('in collection');
55+
expect(count.textContent).not.toContain('162');
56+
expect(count.textContent).not.toContain('ms');
57+
});
58+
59+
it('does not apply the slow-query tint, even for a slow (>1s) query', () => {
60+
const root = renderList({ totalCount: 162, lastQueryMs: 5000 });
61+
const count = root.querySelector('.record-count');
62+
expect(count.classList.contains('record-count-slow')).toBe(false);
63+
});
64+
65+
it('still shows "No records" when there are no records', () => {
66+
const root = renderList({ records: [], totalCount: 162, lastQueryMs: 277 });
67+
expect(root.querySelector('.record-count').textContent).toBe('No records');
68+
});
69+
});

0 commit comments

Comments
 (0)