Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
721ca08
feat(ui): add entity list explorer
johanpel Jul 16, 2026
01f23fa
refactor(ui): split entity list components
johanpel Jul 16, 2026
8bec1f9
feat(ui): per-resource long-entities Gantt; drop timeline entity marks
johallar Jul 23, 2026
349013a
refactor: always include entities row under leaves, move label to top…
johallar Jul 23, 2026
684e968
feature: operator gantt chart tooltip, sync hover line
johallar Jul 28, 2026
950e896
refactor: update entities gantt chart for echarts 6 paradigm (seconds…
johallar Jul 28, 2026
36bdc69
refactor: shrink long entities max height
johallar Jul 28, 2026
511c85c
refactor: user facing messaging -> entities
johallar Jul 30, 2026
0620be1
test(components): align entity hover tests with elapsed time
johallar Jul 30, 2026
b25023c
chore(ui): canonicalize entity UI license headers
johallar Jul 30, 2026
8efe45c
Constant height swimlane rows, keep previous data
johallar Aug 3, 2026
ada35dc
Move labels to inside boxes, tweak long entity threshold
johallar Aug 3, 2026
45c30f8
Label entity/operator rows
johallar Aug 3, 2026
082284d
Simple pagination for entities gantt charts
johallar Aug 3, 2026
bc5a38c
Skeleton loader instead of text
johallar Aug 3, 2026
4e98851
fix: add xs button variant, linting/test fixes
johallar Aug 5, 2026
b5652f9
refactor: move timeline settings to new component, but remove from to…
johallar Aug 5, 2026
83593c1
refactor: consolidate colordot, colorcircle; only show entities that …
johallar Aug 5, 2026
147e20d
refactor(ui): preserve API ordering before inserting new entities, le…
johallar Aug 5, 2026
457f1e9
Merge remote-tracking branch 'joe/joe/long-entities-gantt-3' into ent…
cmatzenbach Aug 7, 2026
2d30577
Utilize thin scrollbar
cmatzenbach Aug 7, 2026
e7e181a
Make table header sticky; add jump to page input box
cmatzenbach Aug 7, 2026
fd3cde0
Verbiage fix; display operator label instead of id
cmatzenbach Aug 7, 2026
51028e2
Pass down operator label
cmatzenbach Aug 7, 2026
be3e71d
Allow scrolling select with arrow keys
cmatzenbach Aug 7, 2026
07a9f05
Combine hooks, update callers and tests
cmatzenbach Aug 7, 2026
f38b804
Only debounce input fields; use name+idx for key
cmatzenbach Aug 7, 2026
e2d4f9f
Slide out entity detail drawer when entity is click on timeline
cmatzenbach Aug 7, 2026
f09f4ed
Create drawer to display entity details from clicking on entities in …
cmatzenbach Aug 10, 2026
0e5d4f0
Visual upgrades - highlight high states, apply coloring, format capac…
cmatzenbach Aug 10, 2026
92c9f1d
Add duration bars to each FSM state
cmatzenbach Aug 10, 2026
407e83c
Create fsm memory capacity chart component and export
cmatzenbach Aug 10, 2026
a730163
Render capacity chart in entity details panel
cmatzenbach Aug 10, 2026
1e2b908
Align colors with gant; highlight selected task; clear selection with…
cmatzenbach Aug 10, 2026
b133551
Add QueryToolbar to entities table
cmatzenbach Aug 10, 2026
43510eb
make select labels more consistent; improve field wrapping structure
cmatzenbach Aug 10, 2026
7e51beb
add column sorting; fix clear button in combobox
cmatzenbach Aug 10, 2026
b0ce555
Add serial state transition bar to table
cmatzenbach Aug 11, 2026
bb4afff
Add state % bar to top of details panel
cmatzenbach Aug 11, 2026
0984185
Add tooltip showing % of time spent in each state
cmatzenbach Aug 11, 2026
61ea434
Merge branch 'main' into entities-panel
cmatzenbach Aug 11, 2026
db8c799
linting
cmatzenbach Aug 11, 2026
dc05b4c
fix: adapt entity list consumers to unwrapped FSM response format
cmatzenbach Aug 11, 2026
30a8889
fix: don't commit generated ts bindings
cmatzenbach Aug 11, 2026
58cf8c9
refactor: use helpers from formatters.ts
cmatzenbach Aug 11, 2026
db891b6
fix: keep bigint values for tooltip formatting
cmatzenbach Aug 11, 2026
c641c0b
test error states
cmatzenbach Aug 11, 2026
08f10c9
feat(entities): add keyboard and screen reader accessibility to state…
cmatzenbach Aug 11, 2026
4233e2b
fix license headers
cmatzenbach Aug 11, 2026
bc71a67
add keyboard control + screen reader support to table header
cmatzenbach Aug 11, 2026
84822e2
make drawer subtree inert when fsm is null
cmatzenbach Aug 11, 2026
626de61
Merge branch 'main' into entities-panel
cmatzenbach Aug 20, 2026
00dcf87
Update per api changes
cmatzenbach Aug 20, 2026
23805ca
Merge branch 'main' into entities-panel
cmatzenbach Aug 24, 2026
e0c777f
fix: remove FE sort, use BE sort; remove local type, use EntityListRe…
cmatzenbach Aug 24, 2026
0b0765e
fix: update tests
cmatzenbach Aug 24, 2026
7b64947
fix: unify styling of header controls
cmatzenbach Aug 24, 2026
00c0209
fix: use label element like other fields
cmatzenbach Aug 24, 2026
9cd5a7a
refactor: wrap table cells displaying data in DataText component
cmatzenbach Aug 24, 2026
4302e8b
refactor: replace strings with global consts
cmatzenbach Aug 24, 2026
ba8f052
fix: de-select table row when clicking on it again
cmatzenbach Aug 24, 2026
0396fb0
fix: modify tsconfig to fix shadcn imports; import pagination component
cmatzenbach Aug 24, 2026
c7ec57e
refactor: expose pagination components; utilize in EntityTable in pla…
cmatzenbach Aug 24, 2026
fe5b2f9
formatting/linting
cmatzenbach Aug 24, 2026
a6eab6f
fix: only show loading when query is fetching
cmatzenbach Aug 25, 2026
62753d6
fix: keep entities whos lifecycles are inside of window, even if lack…
cmatzenbach Aug 26, 2026
73c70ca
refactor: group useEntityTable return value by filters/pagination/que…
cmatzenbach Aug 26, 2026
3f62512
fix: stop toolbar Reset button from floating after sort-by field removal
cmatzenbach Aug 26, 2026
47ff66a
refactor: use shadcn validation mechanisms
cmatzenbach Aug 26, 2026
1b6e6e2
refactor: remove data volume calcs and display
cmatzenbach Aug 26, 2026
022fd79
refactor: use constants instead of magic strings
cmatzenbach Aug 26, 2026
df7aa54
add shadcn slider component, export
cmatzenbach Aug 26, 2026
c0c65fd
Add shadcn slider field, create double-ended slider, export new compo…
cmatzenbach Aug 26, 2026
9e9646b
feat: add slider to min usage and window fields
cmatzenbach Aug 26, 2026
1a4ba64
fix: add rounding, fix font size to match other input fields defaults
cmatzenbach Aug 26, 2026
6b6e032
refactor: use overlay for loading indicator; re-organize bottom row i…
cmatzenbach Aug 26, 2026
6e4994c
refactor: move helpers to separate folder, rename to match convention
cmatzenbach Aug 27, 2026
12e90d7
refactor: use max of longest-running entity for window slider max
cmatzenbach Aug 27, 2026
9cbfb85
Merge branch 'main' into entities-panel
cmatzenbach Aug 27, 2026
7220230
linting
cmatzenbach Aug 27, 2026
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
11 changes: 10 additions & 1 deletion crates/ui/src/entities/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@
use serde::Serialize;
use ts_rs::TS;

use quent_time::TimeSec;

use crate::FiniteStateMachine;

/// An entity and its longest matching resource usage.
#[derive(TS, Debug, Clone, Serialize)]
pub struct EntityListItem {
pub entity: FiniteStateMachine,
pub usage_duration_s: TimeSec,
}

/// A ranked, paged list of entities.
#[derive(TS, Debug, Clone, Serialize)]
pub struct EntityListResponse {
// TODO(johanpel): generalize to other entity types, but only FSMs are
// represented today.
pub items: Vec<FiniteStateMachine>,
pub items: Vec<EntityListItem>,
/// The count of entities matching the filter before paging.
pub total: u32,
}
33 changes: 26 additions & 7 deletions domains/query_engine/analyzer/src/entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
use std::collections::HashSet;

use quent_analyzer::{
AnalyzerResult,
fsm::{FsmUsages, collection::FsmCollection},
AnalyzerResult, Span,
fsm::{Fsm, FsmUsages, collection::FsmCollection},
resource::Usage,
};
use quent_time::{TimeNanoSec, TimeUnixNanoSec, span::SpanUnixNanoSec, to_nanosecs};
use quent_time::{TimeNanoSec, TimeUnixNanoSec, span::SpanUnixNanoSec, to_nanosecs, to_secs};
use quent_ui::{
FiniteStateMachine,
entities::{
request::{EntityListFilter, EntitySortKey, Sort, SortDir},
response::EntityListResponse,
response::{EntityListItem, EntityListResponse},
},
paginate::PageParams,
};
Expand Down Expand Up @@ -111,14 +111,24 @@ where
};

let items = page_iter
.map(|(f, _)| FiniteStateMachine::try_from_fsm(f, epoch))
.collect::<Result<Vec<_>, _>>()?;
.map(|(f, usage_duration)| {
FiniteStateMachine::try_from_fsm(f, epoch).map(|entity| EntityListItem {
usage_duration_s: to_secs(usage_duration),
entity,
})
})
.collect::<Result<Vec<_>, quent_time::TimeError>>()?;

Ok(EntityListResponse { items, total })
}

/// The longest single usage span within the window on a scope resource, or any
/// resource when `scope` is `None`.
///
/// When `scope` is `None`, an entity may have no usages overlapping the
/// window at all yet still belong in the window — it's kept as long as its
/// overall lifecycle (first event to last event) overlaps the window, even if
/// no single event falls inside it (e.g. it started before and ended after).
fn usage_metric<'a, F>(
fsm: &'a F,
scope: Option<&HashSet<Uuid>>,
Expand All @@ -136,6 +146,15 @@ where

match scope {
Some(_) => longest,
None => Some(longest.unwrap_or(0)),
None => entity_overlaps_window(fsm, window).then(|| longest.unwrap_or(0)),
}
}

/// Whether `fsm`'s overall lifecycle span (its first event to its last event)
/// overlaps `window` at all.
fn entity_overlaps_window<F>(fsm: &F, window: SpanUnixNanoSec) -> bool
where
F: Fsm,
{
fsm.span().is_ok_and(|span| span.intersects(&window))
}
92 changes: 91 additions & 1 deletion domains/query_engine/tests/fixed/tests/list_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn request(
}

fn ids(resp: &EntityListResponse) -> Vec<Uuid> {
resp.items.iter().map(|fsm| fsm.id).collect()
resp.items.iter().map(|item| item.entity.id).collect()
}

#[test]
Expand All @@ -143,6 +143,7 @@ fn lists_all_tasks_on_a_resource_ranked_by_uuid_tiebreak() {

assert_eq!(resp.total, 8);
assert_eq!(ids(&resp), MEMORY_W0_TASKS);
assert!(resp.items.iter().all(|item| item.usage_duration_s == 0.75));
}

#[test]
Expand Down Expand Up @@ -211,6 +212,95 @@ fn pagination_slices_the_ranked_set_with_stable_total() {
assert_eq!(ids(&page2), MEMORY_W0_TASKS[6..8]);
}

#[test]
fn unscoped_window_excludes_entities_whose_span_never_overlaps() {
let analyzer = fixed_analyzer();

// The window is relative to the query's own epoch (1.0s absolute in this
// fixture). No task's lifecycle (first event to last event) reaches
// [6.5, 7.0) relative (7.5-8.0s absolute): the latest event of any task is
// TASK_10/TASK_11's exit at 6.0s absolute (5.0s relative). With no scope
// and no min-usage filter, the window alone must still exclude entities
// whose lifecycle never overlaps it.
let request = EntityListRequest {
entry: EntityListEntry {
window: TimeWindow {
start: 6.5,
end: 7.0,
},
..entry(None, None, None, Vec::new())
},
app_params: QueryFilter {
query_id: fixed::QUERY,
},
};
let resp = analyzer.list_entities(request).unwrap();

assert_eq!(resp.total, 0);
assert!(resp.items.is_empty());
}

#[test]
fn unscoped_window_includes_entities_whose_span_overlaps() {
let analyzer = fixed_analyzer();

// The window is relative to the query's own epoch (its `init` transition,
// at 1.0s absolute in this fixture). TASK_0..3 run 2.0-3.0s absolute, i.e.
// [1.0, 2.0) relative; every other task's lifecycle starts at 3.0s
// absolute (2.0s relative) or later, so only TASK_0..3 overlap [1.0, 1.3).
let request = EntityListRequest {
entry: EntityListEntry {
window: TimeWindow {
start: 1.0,
end: 1.3,
},
..entry(None, None, None, Vec::new())
},
app_params: QueryFilter {
query_id: fixed::QUERY,
},
};
let resp = analyzer.list_entities(request).unwrap();

let mut got = ids(&resp);
got.sort();
let mut want = vec![fixed::TASK_0, fixed::TASK_1, fixed::TASK_2, fixed::TASK_3];
want.sort();
assert_eq!(resp.total, 4);
assert_eq!(got, want);
}

#[test]
fn unscoped_window_includes_entities_even_without_an_event_inside_it() {
let analyzer = fixed_analyzer();

// TASK_4..7 (PartialAggregate) run 3.0-4.0s absolute (2.0-3.0s relative).
// A window strictly between their `computing`/`sending` events and their
// `exit` contains none of their individual event timestamps, but their
// overall lifecycle still overlaps it, so they must still be included —
// this is the exact case the window filter previously got wrong.
let request = EntityListRequest {
entry: EntityListEntry {
window: TimeWindow {
start: 2.6,
end: 2.7,
},
..entry(None, None, None, Vec::new())
},
app_params: QueryFilter {
query_id: fixed::QUERY,
},
};
let resp = analyzer.list_entities(request).unwrap();

let mut got = ids(&resp);
got.sort();
let mut want = vec![fixed::TASK_4, fixed::TASK_5, fixed::TASK_6, fixed::TASK_7];
want.sort();
assert_eq!(resp.total, 4);
assert_eq!(got, want);
}

#[test]
fn operator_filter_restricts_to_one_operator_tasks() {
let analyzer = fixed_analyzer();
Expand Down
27 changes: 27 additions & 0 deletions ui/packages/@quent/client/src/entityList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,30 @@ export const useEntityList = (
params: EntityListParams,
options?: { staleTime?: number; enabled?: boolean }
) => useQuery(entityListQueryOptions(params, options));

// Raw-request variant: accepts a pre-built EntityListRequest directly.
// Used when the caller constructs the full request object itself.
interface EntitiesParams {
engineId: string;
request: EntityListRequest<QueryFilter, OperatorFilter>;
}

interface EntitiesOptions {
staleTime?: number;
enabled?: boolean;
}

export const entitiesQueryOptions = (
{ engineId, request }: EntitiesParams,
options?: EntitiesOptions
) =>
queryOptions({
queryKey: ['entities', engineId, request],
queryFn: () => fetchEntityList(engineId, request),
staleTime: options?.staleTime ?? DEFAULT_STALE_TIME,
enabled: options?.enabled,
placeholderData: keepPreviousData,
});

export const useEntities = (params: EntitiesParams, options?: EntitiesOptions) =>
useQuery(entitiesQueryOptions(params, options));
3 changes: 2 additions & 1 deletion ui/packages/@quent/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export { queriesQueryOptions } from './queries';
export { singleTimelineQueryOptions } from './timeline';
export { bulkTimelineQueryOptions } from './bulkTimelines';
export { dataFlowQueryOptions } from './dataFlow';
export { entityListQueryOptions } from './entityList';
export { entitiesQueryOptions, entityListQueryOptions } from './entityList';
export {
canonicalizeNvtxRequest,
canonicalizeNvtxSelections,
Expand All @@ -46,6 +46,7 @@ export { useEngines } from './engines';
export { useQueryGroups } from './queryGroups';
export { useQueries } from './queries';
export { useTimeline } from './timeline';
export { useEntities } from './entityList';
export { useDataFlow } from './dataFlow';
export { useEntityList } from './entityList';
export { useEngineContexts, useNvtxCatalog, useNvtxStream, useNvtxViewport } from './nvtx';
16 changes: 16 additions & 0 deletions ui/packages/@quent/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export {
NavigationMenuIndicator,
NavigationMenuViewport,
} from './ui/navigation-menu';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from './ui/pagination';
export { Popover, PopoverTrigger, PopoverContent } from './ui/popover';
export { PointerTooltipPortal } from './ui/pointer-tooltip-portal';
export type { PointerPosition } from './ui/pointer-tooltip-portal';
Expand Down Expand Up @@ -82,12 +91,19 @@ export {
} from './ui/select';
export { SelectField } from './ui/select-field';
export type { SelectFieldProps, SelectFieldOption } from './ui/select-field';
export { SearchableSelect } from './ui/searchable-select';
export type { SearchableSelectProps } from './ui/searchable-select';
export { RequiredMultiSelectField } from './ui/required-multi-select-field';
export type {
RequiredMultiSelectFieldProps,
RequiredMultiSelectOption,
} from './ui/required-multi-select-field';
export { Skeleton } from './ui/skeleton';
export { Slider } from './ui/slider';
export { SliderField } from './ui/slider-field';
export type { SliderFieldProps } from './ui/slider-field';
export { RangeSliderField } from './ui/range-slider-field';
export type { RangeSliderFieldProps } from './ui/range-slider-field';
export { TreeView } from './ui/tree-view';
export type { TreeDataItem } from './ui/tree-view';
export { TreeTable } from './ui/tree-table';
Expand Down
31 changes: 31 additions & 0 deletions ui/packages/@quent/components/src/lib/sliderField.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export function clamp(value: number, min: number, max: number): number {
Comment thread
cmatzenbach marked this conversation as resolved.
return Math.min(max, Math.max(min, value));
}

export function parseOptionalNumber(value: string): number | null {
if (value.trim() === '') {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}

/** Picks a "nice" step (1/2/5 * 10^n) so a slider spanning `span` units has roughly 100-200 stops. */
export function niceSliderStep(span: number): number {
if (!Number.isFinite(span) || span <= 0) {
return 0.1;
}
const rawStep = span / 150;
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
const normalized = rawStep / magnitude;
const niceNormalized = normalized < 1.5 ? 1 : normalized < 3.5 ? 2 : normalized < 7.5 ? 5 : 10;
return niceNormalized * magnitude;
}

export function formatStep(value: number, step: number): string {
const decimals = step > 0 && step < 1 ? Math.min(6, Math.ceil(-Math.log10(step))) : 0;
return value.toFixed(decimals);
}
2 changes: 1 addition & 1 deletion ui/packages/@quent/components/src/long-entities/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function buildSegments(
colorFsm: (stateName: string) => string,
resourceIdsForFilter?: ReadonlySet<string> | null
): LongEntitySegment[] {
return fsm.transitions
return (fsm.transitions ?? [])
.slice(0, -1)
.map((transition, i): LongEntitySegment | null => {
const next = fsm.transitions[i + 1];
Expand Down
2 changes: 1 addition & 1 deletion ui/packages/@quent/components/src/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
className
)}
ref={ref}
Expand Down
Loading