Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,10 @@ describe('ExplorerPage independent lane lifecycle', () => {
expect(knowledge.queryByText('0')).toBeNull();
expect(knowledge.getByText('UNAVAILABLE')).toBeTruthy();

// The ready lanes carry their own counts and grade, untouched.
// The ready lanes carry their own counts, grade and freshness, untouched.
expect(within(lane('Code')).getByText('1')).toBeTruthy();
expect(within(lane('Code')).getByText('EXACT')).toBeTruthy();
expect(lane('Code').querySelector('[data-lane-freshness="unknown"]')).toBeTruthy();
expect(within(lane('Sessions')).getByText('2')).toBeTruthy();

// The summary counts only lanes that answered.
Expand Down Expand Up @@ -439,6 +440,10 @@ describe('ExplorerPage scope truth', () => {
within(lane('Code')).getAllByText(/Other project is not the active project/).length,
).toBeGreaterThan(0);
expect(within(lane('Code')).getByText('Read-only scope')).toBeTruthy();
// The run register carries the same refusal rather than a pending admission.
const register = document.querySelector('[data-run-register]');
expect(register?.getAttribute('data-run-state')).toBe('locked');
expect(within(register as HTMLElement).getByText(/no run created/)).toBeTruthy();
expect(
fetchImpl.mock.calls.some(([input]) => String(input).includes('/api/explorer/queries')),
).toBe(false);
Expand Down
19 changes: 15 additions & 4 deletions dashboard/src/workspaces/explorer/ExplorerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export function ExplorerPage() {
<RunRegister
result={explorer.runResult}
run={explorer.run}
writability={explorer.writability}
progress={explorer.runProgress}
cancelling={explorer.cancelling}
onCancel={explorer.cancel}
Expand Down Expand Up @@ -159,7 +160,8 @@ export function ExplorerPage() {
return (
<FilterSelect
key={laneId}
label={`${spec.label} ${spec.facetLabel}`}
label={spec.facetLabel}
name={`${spec.label} ${spec.facetLabel}`}
value={explorer.facet?.lane === laneId ? explorer.facet.value : ''}
onChange={(value) =>
explorer.setFacet(value === '' ? null : { lane: laneId, value })
Expand Down Expand Up @@ -245,25 +247,34 @@ function asLaneId(value: string): LaneId | null {
}

/** A native select behind an engraved legend, so every filter is one control
* with one accessible name and the platform's own keyboard behaviour. */
* with one accessible name and the platform's own keyboard behaviour.
*
* `name` widens the accessible name past the visible legend when the legend
* alone would be ambiguous (three lanes each have a facet); the visible text
* stays inside the name, so label-in-name holds. The select is capped rather
* than sized to its widest option, which at 320px pushed the whole register
* past the aperture's edge. */
function FilterSelect({
label,
name,
value,
onChange,
options,
}: {
label: string;
name?: string;
value: string;
onChange: (value: string) => void;
options: readonly { value: string; label: ReactNode }[];
}) {
return (
<label className="flex min-h-[var(--touch-target-min)] items-center gap-2 border border-edge-subtle bg-surface-0 pl-2 pr-1 focus-within:border-accent">
<label className="flex min-h-[var(--touch-target-min)] min-w-0 max-w-full items-center gap-2 border border-edge-subtle bg-surface-0 pl-2 pr-1 focus-within:border-accent">
<span className="td-legend">{label}</span>
<select
value={value}
aria-label={name}
onChange={(event) => onChange(event.target.value)}
className="min-h-[calc(var(--touch-target-min)-2px)] bg-transparent pr-1 text-xs text-text-primary outline-none"
className="min-h-[calc(var(--touch-target-min)-2px)] min-w-0 max-w-[10rem] bg-transparent pr-1 text-xs text-text-primary outline-none"
>
{options.map((option) => (
<option key={option.value} value={option.value}>
Expand Down
94 changes: 73 additions & 21 deletions dashboard/src/workspaces/explorer/Lane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* worked. Nothing in a lane is computed from another lane.
*/
import { type KeyboardEvent, type ReactNode } from 'react';
import { EvidencePattern } from '../../ui/EvidencePattern.tsx';
import { StateChip } from '../../ui/StateChip';
import { VirtualList } from '../../ui/VirtualList.tsx';
import { Highlight } from '../../ui/search/Highlight.tsx';
Expand All @@ -17,6 +18,7 @@ import { compactRelativeAge } from '../../ui/time.ts';
import { laneGrade } from './evidence.ts';
import { LANE_BY_ID, LANE_ICON } from './laneChrome.ts';
import {
laneEvidence,
laneHits,
laneStateDetail,
laneStateKind,
Expand All @@ -27,6 +29,12 @@ import type { Hit } from './model.ts';
/** Fixed row height shared with the virtualizer: title, location, meta. */
export const LANE_ROW_HEIGHT = 60;

/** A code-graph generation watermark is a long dotted digest; the footer
* shows enough to tell two apart and carries the whole value as a title. */
function clipWatermark(watermark: string): string {
return watermark.length <= 28 ? watermark : `${watermark.slice(0, 27)}…`;
}

/** Marks each lane's row region so the grid can traverse lanes with the
* horizontal arrows without knowing how a lane lays itself out. */
export const LANE_LIST_ATTR = 'data-lane-list';
Expand Down Expand Up @@ -69,12 +77,21 @@ export function Lane({
className="relative flex min-h-[var(--pane-min-height)] min-w-0 flex-col border border-edge-subtle bg-surface-1"
>
<Corners tone={read.state === 'ready' ? 'signal' : 'edge'} />
<header className="flex min-h-9 shrink-0 flex-wrap items-center gap-x-2 gap-y-1 border-b border-edge-subtle px-2.5 py-1.5">
<span aria-hidden className={cn('h-3 w-[3px] shrink-0', spec.railClass)} />
<h2 className="td-title text-text-primary">{spec.label}</h2>
<StateChip kind={kind} detail={laneStateDetail(read)} className="max-w-full" />
<span aria-hidden className="td-rule" />
<Icon aria-hidden size={14} className={cn('shrink-0', spec.textClass)} />
{/* Name over state, so four headers align whatever length a state's
* detail runs to. The chip truncates its detail; the body repeats the
* full sentence for any lane that has no rows to show instead. */}
<header className="flex shrink-0 flex-col gap-1 border-b border-edge-subtle px-2.5 py-1.5">
<span className="flex h-5 items-center gap-2">
<span aria-hidden className={cn('h-3 w-[3px] shrink-0', spec.railClass)} />
<h2 className="td-title text-text-primary">{spec.label}</h2>
<span aria-hidden className="td-rule" />
<Icon aria-hidden size={14} className={cn('shrink-0', spec.textClass)} />
</span>
<StateChip
kind={kind}
detail={laneStateDetail(read)}
className="w-fit max-w-full [&>span:last-child]:truncate"
/>
</header>

{/* The headline count, or the absence of one. A lane that did not
Expand All @@ -88,16 +105,24 @@ export function Lane({
</span>
</div>
<span className="flex items-center justify-between gap-2 text-3xs text-text-muted">
<span className="truncate">
{answered
? read.state === 'partial'
? 'loaded from an incomplete read; records were omitted'
: total != null
? `loaded of ${total.toLocaleString()} matching reported`
: searching
? 'loaded; no source total reported'
: 'shown from the overview endpoint'
: 'no count reported'}
<span className="flex min-w-0 items-center gap-1.5">
{/* How well the count is known, on the shared pattern axis: solid
* when the source reported a real denominator, hatched when rows
* arrived without one. Distinct from the record grade beside it. */}
{answered ? (
<EvidencePattern quality={laneEvidence(read)} className="shrink-0 text-3xs" />
) : null}
<span className="truncate">
{answered
? read.state === 'partial'
? 'loaded from an incomplete read; records were omitted'
: total != null
? `loaded of ${total.toLocaleString()} matching reported`
: searching
? 'loaded; no source total reported'
: 'shown from the overview endpoint'
: 'no count reported'}
</span>
</span>
{grade !== null ? (
<span
Expand Down Expand Up @@ -132,8 +157,24 @@ export function Lane({
<span className="td-value text-3xs text-text-muted">
{answered ? `${rows.length.toLocaleString()} shown` : '—'}
{answered && read.hasMore === true ? ' · more rows remain past this page' : ''}
{/* The source's own freshness word and watermark, so a served page
* says how current it is rather than looking current by default. */}
{answered ? (
<span data-lane-freshness={read.freshness} title={read.watermark ?? undefined}>
{` · ${read.freshness}`}
{read.watermark !== null ? ` @ ${clipWatermark(read.watermark)}` : ''}
</span>
) : null}
</span>
<span>
{answered
? 'source order · no cross-lane rank'
: read.state === 'pending'
? 'reading'
: searching
? 'not served'
: 'no rows'}
</span>
<span>{answered ? 'source order · no cross-lane rank' : searching ? 'not served' : 'no rows'}</span>
</footer>
</section>
);
Expand Down Expand Up @@ -343,9 +384,11 @@ function LaneRow({
className={cn(
'relative flex w-full flex-col justify-center gap-0.5 border-b border-edge-subtle pl-3.5 pr-2.5 text-left',
'transition-[opacity,background-color,box-shadow] duration-[var(--dur-state)] ease-[var(--ease-standard)] motion-reduce:transition-none',
// Hover raises the face one plane; selection is the gutter, not a wash,
// so the two never read as the same thing.
'hover:bg-raised hover:shadow-[var(--shadow-raised)] focus-visible:bg-surface-2',
// Hover raises the face one plane and draws one restrained halo;
// selection is the gutter, not a wash, so the two never read as the
// same thing. Keyboard focus keeps the shell's 2px outline.
'hover:bg-raised hover:shadow-[var(--shadow-raised)] hover:outline hover:outline-1 hover:-outline-offset-1 hover:outline-accent/40',
'focus-visible:bg-surface-2 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent',
selected && 'bg-surface-2',
)}
>
Expand Down Expand Up @@ -468,7 +511,16 @@ export function LaneGrid({ children }: { children: ReactNode }) {
role="group"
aria-label="Result lanes"
onKeyDown={onKeyDown}
className="td-stagger grid min-h-0 flex-1 auto-rows-fr gap-2 p-2 [grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))]"
className={cn(
'td-stagger grid gap-2 p-2 [grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))]',
// One row at `lg` and above: the grid takes the aperture's height and
// each lane scrolls its own rows. Where the lanes wrap — narrow
// viewports, 200% zoom — every row is a fixed lane height instead of
// a share of a height too small to hold them, so wrapped lanes stack
// and the page scrolls rather than the rows overlapping.
'lg:min-h-0 lg:flex-1 lg:auto-rows-fr',
'max-lg:flex-none max-lg:auto-rows-[28rem]',
)}
>
{children}
</div>
Expand Down
5 changes: 5 additions & 0 deletions dashboard/src/workspaces/explorer/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
scopedUrl,
useScope,
type DashboardScope,
type ScopeWritability,
} from '../../data/scope/store.ts';
import { queryTerms } from '../../ui/search/terms.ts';
import { absenceVerdict, type AbsenceVerdict } from './absence.ts';
Expand Down Expand Up @@ -170,6 +171,9 @@ export interface ExplorerController {
readonly submitted: string;
readonly searching: boolean;
readonly terms: readonly string[];
/** Whether the current scope accepts a query run, from the scope authority.
* Anything but `writable` means no run was, or will be, dispatched. */
readonly writability: ScopeWritability;
/** One read model per lane, in `LANES` order — four, including semantic. */
readonly lanes: readonly ExplorerLaneReadModel[];
/** The lanes the current lane filter admits. */
Expand Down Expand Up @@ -428,6 +432,7 @@ export function useExplorerController(): ExplorerController {
submitted,
searching,
terms,
writability,
lanes,
visibleLanes,
laneRows,
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/workspaces/explorer/evidence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ describe('laneGrade', () => {
reportedTotal: 0,
unreadableRows: 0,
hasMore: false,
freshness: 'fresh',
watermark: null,
};
const stale: ExplorerLaneReadModel = { state: 'stale', lane: 'code', errorCode: null, detail: null };
const pending: ExplorerLaneReadModel = { state: 'pending', lane: 'code', phase: 'reading' };
Expand Down
13 changes: 13 additions & 0 deletions dashboard/src/workspaces/explorer/laneModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ describe('laneFromSourceProgress', () => {
reportedTotal: 0,
unreadableRows: 0,
hasMore: false,
freshness: 'unknown',
watermark: null,
});
expect(new Set([...states, readyEmpty.state]).size).toBe(4);
});
Expand Down Expand Up @@ -248,9 +250,20 @@ describe('laneFromSourceProgress', () => {
reportedTotal: null,
unreadableRows: 0,
hasMore: null,
freshness: 'unknown',
watermark: null,
});
});

it('carries the source\u2019s own freshness word and watermark verbatim', () => {
const fresh = laneFromSourceProgress(
'code',
progress({ freshness: 'fresh', watermark: 'g-42' }),
[],
);
expect(fresh).toMatchObject({ state: 'ready', freshness: 'fresh', watermark: 'g-42' });
});

it('reports rows it could not read instead of silently returning fewer', () => {
const read = laneFromSourceProgress(
'code',
Expand Down
17 changes: 17 additions & 0 deletions dashboard/src/workspaces/explorer/laneModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export type ExplorerLaneReadModel =
/** Whether the source said rows remain past this page; `null` when the
* read carries no pagination at all (the browse overviews). */
readonly hasMore: boolean | null;
/** The source's own freshness word (`fresh`, `unknown`, …) and the
* watermark it answered from, verbatim. Never inferred. */
readonly freshness: string;
readonly watermark: string | null;
}
/** The source answered with real rows but its own read reported omitted
* records: what is shown is genuine and less than what exists. */
Expand All @@ -102,6 +106,8 @@ export type ExplorerLaneReadModel =
readonly reportedTotal: number | null;
readonly unreadableRows: number;
readonly hasMore: boolean | null;
readonly freshness: string;
readonly watermark: string | null;
readonly errorCode: string | null;
readonly detail: string | null;
}
Expand Down Expand Up @@ -302,6 +308,8 @@ export function laneFromSourceProgress(
reportedTotal: null,
unreadableRows: 0,
hasMore: null,
freshness: source.freshness,
watermark: source.watermark,
};
}
const hits = hitsForLane(lane, narrowPageRows(page), terms);
Expand All @@ -312,6 +320,8 @@ export function laneFromSourceProgress(
reportedTotal: page.total,
unreadableRows: page.rows.length - hits.length,
hasMore: page.next_offset !== null,
freshness: source.freshness,
watermark: source.watermark,
};
}
case 'partial': {
Expand All @@ -324,6 +334,8 @@ export function laneFromSourceProgress(
reportedTotal: page?.total ?? null,
unreadableRows: page === null ? 0 : page.rows.length - hits.length,
hasMore: page === null ? null : page.next_offset !== null,
freshness: source.freshness,
watermark: source.watermark,
errorCode: source.error_code,
detail: source.message,
};
Expand Down Expand Up @@ -450,13 +462,18 @@ export function browseLane<T>(
}
const rows = rowsOf(result.envelope.payload);
const hits = hitsForLane(lane, rows, terms);
// The overview carries no per-source freshness; the envelope's own
// freshness reading is the daemon's statement about this read.
const freshness = result.envelope.freshness;
return {
state: 'ready',
lane,
hits,
reportedTotal: null,
unreadableRows: rows.length - hits.length,
hasMore: null,
freshness: freshness.state,
watermark: freshness.watermark,
};
}

Expand Down
Loading
Loading