Skip to content

Commit 2e92bad

Browse files
authored
feat(ui): add "N in progress" indicator and clean up filter layout (#201)
## Summary ### "N in progress" indicator A small indicator button now sits next to the **Status** dropdown in `RunFilters` (used on both `/runs` and the suite detail page). It shows a pulsing blue dot + live-run count (e.g. `● 3 in progress`), shares the same box height and text size as the adjacent dropdowns, and clicking it toggles the existing "In progress" status filter on/off. - Rendered only when `liveRunsCount > 0` — zero visual noise when nothing is live. - On the suite detail page the count is scoped to live runs for the current suite. ### Fix status filter on suite detail page The suite detail page's filter logic was missing the `status === 'running'` case (so choosing "In progress" did nothing), and its `status === 'passing' / 'failing'` checks used `total - passed` regardless of whether the row is live. Now matches the RunsPage logic: live rows use the reported `tests_failed` for pass/fail filtering, and `running` filters down to in-progress rows. ### Cleaner header layout on /runs - Row 1: `Runs (N)` title - Row 2: `Metric steps:` row - Row 3: all filter dropdowns (Client, Image, Suite, Strategy, Status, Live indicator) — left-aligned, wrapping on narrow screens ## Test plan - [x] TypeScript + ESLint clean - [x] Manual: start a live run, confirm the indicator appears on `/runs` next to the Status dropdown with the correct count - [x] Manual: click the indicator, confirm the runs table filters to only in-progress rows; click again to clear - [x] Manual: open the suite detail page for that suite, confirm the indicator appears and is scoped to this suite - [x] Manual: select "In progress" from the Status dropdown on the suite detail page, confirm the table filters correctly (was broken before this PR) - [x] Manual: confirm no indicator renders when no live runs are present
1 parent 784b89a commit 2e92bad

3 files changed

Lines changed: 98 additions & 53 deletions

File tree

ui/src/components/runs/RunFilters.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ interface RunFiltersProps {
2929
suiteLabelKeys?: Map<string, string[]>
3030
suiteLabelFilters?: LabelFiltersType
3131
onSuiteLabelFiltersChange?: (filters: LabelFiltersType) => void
32+
/** Number of currently-live runs. When > 0, a small indicator is shown
33+
* next to the Status dropdown. */
34+
liveRunsCount?: number
35+
/** Called when the user clicks the live indicator. */
36+
onLiveRunsIndicatorClick?: () => void
3237
}
3338

3439
function ChevronIcon() {
@@ -124,6 +129,8 @@ export function RunFilters({
124129
suiteLabelKeys,
125130
suiteLabelFilters,
126131
onSuiteLabelFiltersChange,
132+
liveRunsCount = 0,
133+
onLiveRunsIndicatorClick,
127134
}: RunFiltersProps) {
128135
const clientOptions = [{ value: '' as const, label: 'All clients' }, ...clients.map((c) => ({ value: c, label: c }))]
129136
const imageOptions = [{ value: '' as const, label: 'All images' }, ...images.map((i) => ({ value: i, label: i }))]
@@ -184,6 +191,32 @@ export function RunFilters({
184191
allLabel="All runs"
185192
width="w-36"
186193
/>
194+
{liveRunsCount > 0 && (
195+
<div className="flex flex-col gap-1">
196+
{/* Empty label keeps vertical alignment with the labeled dropdowns. */}
197+
<label className="text-sm/5 font-medium text-transparent select-none">&nbsp;</label>
198+
<button
199+
onClick={onLiveRunsIndicatorClick}
200+
title={selectedStatus === 'running' ? 'Show all runs' : 'Filter to in-progress runs only'}
201+
className={clsx(
202+
'relative cursor-pointer rounded-sm py-2 pr-3 pl-3 text-left text-sm/6 shadow-xs ring-1 ring-inset transition-colors',
203+
selectedStatus === 'running'
204+
? 'bg-blue-600 text-white ring-blue-600 hover:bg-blue-700 hover:ring-blue-700'
205+
: 'bg-blue-50 text-blue-700 ring-blue-200 hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-200 dark:ring-blue-800 dark:hover:bg-blue-900/50',
206+
)}
207+
>
208+
<span className="flex items-center gap-2">
209+
<span
210+
className={clsx(
211+
'size-1.5 animate-pulse rounded-full',
212+
selectedStatus === 'running' ? 'bg-white' : 'bg-blue-500 dark:bg-blue-400',
213+
)}
214+
/>
215+
{liveRunsCount} in progress
216+
</span>
217+
</button>
218+
</div>
219+
)}
187220
</div>
188221
{suiteLabelKeys && suiteLabelKeys.size > 0 && suiteLabelFilters && onSuiteLabelFiltersChange && (
189222
<LabelFilters entries={[]} filters={suiteLabelFilters} onChange={onSuiteLabelFiltersChange} availableLabels={suiteLabelKeys} addButtonLabel="Suite label" />

ui/src/pages/RunsPage.tsx

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -377,59 +377,60 @@ export function RunsPage() {
377377

378378
return (
379379
<div className="flex flex-col gap-6">
380-
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
381-
<h1 className="text-2xl/8 font-bold text-gray-900 dark:text-gray-100">Runs ({filteredEntries.length})</h1>
382-
<div className="flex flex-col items-end gap-2">
383-
<div className="flex items-center gap-2">
384-
<span className="text-sm/6 font-medium text-gray-700 dark:text-gray-300">Metric steps:</span>
385-
<div className="flex items-center gap-1">
386-
{ALL_INDEX_STEP_TYPES.map((step) => (
387-
<button
388-
key={step}
389-
onClick={() => {
390-
const newFilter = stepFilter.includes(step)
391-
? stepFilter.filter((s) => s !== step)
392-
: [...stepFilter, step]
393-
if (newFilter.length > 0) {
394-
handleStepFilterChange(newFilter)
395-
}
396-
}}
397-
className={`rounded-sm px-2.5 py-1 text-xs font-medium capitalize transition-colors ${
398-
stepFilter.includes(step)
399-
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
400-
: 'bg-gray-100 text-gray-400 dark:bg-gray-700 dark:text-gray-500'
401-
}`}
402-
>
403-
{step}
404-
</button>
405-
))}
406-
</div>
407-
</div>
408-
<div className="flex flex-wrap items-end justify-end gap-4">
409-
<RunFilters
410-
clients={clients}
411-
selectedClient={client}
412-
onClientChange={handleClientChange}
413-
images={images}
414-
selectedImage={image}
415-
onImageChange={handleImageChange}
416-
suites={suites}
417-
selectedSuite={suite}
418-
onSuiteChange={handleSuiteChange}
419-
strategies={strategies}
420-
selectedStrategy={strategy}
421-
onStrategyChange={handleStrategyChange}
422-
selectedStatus={status}
423-
onStatusChange={handleStatusChange}
424-
entries={index?.entries}
425-
labelFilters={labelFilters}
426-
onLabelFiltersChange={handleLabelFiltersChange}
427-
suiteLabelKeys={suiteLabelKeys}
428-
suiteLabelFilters={suiteLabelFilters}
429-
onSuiteLabelFiltersChange={handleSuiteLabelFiltersChange}
430-
/>
380+
<h1 className="text-2xl/8 font-bold text-gray-900 dark:text-gray-100">Runs ({filteredEntries.length})</h1>
381+
382+
<div className="flex flex-col gap-3">
383+
<div className="flex items-center gap-2">
384+
<span className="text-xs/5 font-medium text-gray-700 dark:text-gray-300">Metric steps:</span>
385+
<div className="flex items-center gap-1">
386+
{ALL_INDEX_STEP_TYPES.map((step) => (
387+
<button
388+
key={step}
389+
onClick={() => {
390+
const newFilter = stepFilter.includes(step)
391+
? stepFilter.filter((s) => s !== step)
392+
: [...stepFilter, step]
393+
if (newFilter.length > 0) {
394+
handleStepFilterChange(newFilter)
395+
}
396+
}}
397+
className={`rounded-sm px-2.5 py-1 text-xs font-medium capitalize transition-colors ${
398+
stepFilter.includes(step)
399+
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
400+
: 'bg-gray-100 text-gray-400 dark:bg-gray-700 dark:text-gray-500'
401+
}`}
402+
>
403+
{step}
404+
</button>
405+
))}
431406
</div>
432407
</div>
408+
<div className="flex flex-wrap items-end gap-4">
409+
<RunFilters
410+
clients={clients}
411+
selectedClient={client}
412+
onClientChange={handleClientChange}
413+
images={images}
414+
selectedImage={image}
415+
onImageChange={handleImageChange}
416+
suites={suites}
417+
selectedSuite={suite}
418+
onSuiteChange={handleSuiteChange}
419+
strategies={strategies}
420+
selectedStrategy={strategy}
421+
onStrategyChange={handleStrategyChange}
422+
selectedStatus={status}
423+
onStatusChange={handleStatusChange}
424+
entries={index?.entries}
425+
labelFilters={labelFilters}
426+
onLabelFiltersChange={handleLabelFiltersChange}
427+
suiteLabelKeys={suiteLabelKeys}
428+
suiteLabelFilters={suiteLabelFilters}
429+
onSuiteLabelFiltersChange={handleSuiteLabelFiltersChange}
430+
liveRunsCount={liveRuns?.length ?? 0}
431+
onLiveRunsIndicatorClick={() => handleStatusChange(status === 'running' ? 'all' : 'running')}
432+
/>
433+
</div>
433434
</div>
434435

435436
{paginatedEntries.length === 0 ? (
@@ -580,3 +581,4 @@ export function RunsPage() {
580581
</div>
581582
)
582583
}
584+

ui/src/pages/SuiteDetailPage.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,18 @@ export function SuiteDetailPage() {
536536
return suiteRunsAll.filter((e) => {
537537
if (client && e.instance.client !== client) return false
538538
if (image && e.instance.image !== image) return false
539-
if (status === 'passing' && e.tests.tests_total - e.tests.tests_passed > 0) return false
540-
if (status === 'failing' && e.tests.tests_total - e.tests.tests_passed === 0) return false
539+
// For live runs, failure count means actually-reported failures, not
540+
// "tests not yet passed". Apply the same convention as RunsPage.
541+
{
542+
const failed = e.status === 'running'
543+
? e.tests.tests_failed
544+
: e.tests.tests_total - e.tests.tests_passed
545+
if (status === 'passing' && failed > 0) return false
546+
if (status === 'failing' && failed === 0) return false
547+
}
541548
if (status === 'timeout' && e.status !== 'timeout') return false
542549
if (status === 'cancelled' && e.status !== 'cancelled') return false
550+
if (status === 'running' && e.status !== 'running') return false
543551
for (const [key, allowedValues] of labelFilters) {
544552
const actual = e.metadata?.[key]
545553
if (!actual || !allowedValues.has(actual)) return false
@@ -1210,6 +1218,8 @@ export function SuiteDetailPage() {
12101218
entries={suiteRunsAll}
12111219
labelFilters={labelFilters}
12121220
onLabelFiltersChange={handleLabelFiltersChange}
1221+
liveRunsCount={liveRuns?.filter((lr) => lr.suite_hash === suiteHash).length ?? 0}
1222+
onLiveRunsIndicatorClick={() => handleStatusChange(status === 'running' ? 'all' : 'running')}
12131223
/>
12141224
</div>
12151225
{filteredRuns.length === 0 ? (

0 commit comments

Comments
 (0)