Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Root as DataViewRoot } from '@nemo/common/src/components/DataView/internal';
import {
Root as DataViewRoot,
EditColumnsMenu,
} from '@nemo/common/src/components/DataView/internal';
import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView';
import { RelativeTime } from '@nemo/common/src/components/RelativeTime';
import { StatusBadge } from '@nemo/common/src/components/StatusBadge';
import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState';
import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState';
import { snakeCaseToTitleCase } from '@nemo/common/src/utils/formatters';
import { useGetExperiment, useListExperimentSessions } from '@nemo/sdk/generated/platform/api';
import type { ExperimentSessionResponse } from '@nemo/sdk/generated/platform/schema';
import type {
ExperimentSessionFilter,
ExperimentSessionResponse,
} from '@nemo/sdk/generated/platform/schema';
import { Text, Tooltip } from '@nvidia/foundations-react-core';
import { Empty } from '@studio/components/dataViews/ExperimentSessionsDataView/Empty';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { tooltipClassName } from '@studio/styles/common';
import { keepPreviousData } from '@tanstack/react-query';
import { Columns3 } from 'lucide-react';
import { type ComponentProps, type FC, useMemo } from 'react';

type SessionRow = ExperimentSessionResponse & { _rowId: string };
Expand All @@ -33,7 +41,7 @@ export const ExperimentSessionsDataView: FC<ExperimentSessionsDataViewProps> = (
experimentGroupName,
}) => {
const workspace = useWorkspaceFromPath();
const dataViewState = useStudioDataViewState({});
const dataViewState = useStudioDataViewState<ExperimentSessionFilter>({ columnVisibility: {} });
const { data: experiment } = useGetExperiment(workspace, experimentName);

const page = dataViewState.pagination.state.pageIndex + 1;
Expand All @@ -42,7 +50,16 @@ export const ExperimentSessionsDataView: FC<ExperimentSessionsDataViewProps> = (
const { data: sessionsResponse, isLoading } = useListExperimentSessions(
workspace,
experimentName,
{ page, page_size: pageSize },
{
page,
page_size: pageSize,
filter: {
...dataViewState.apiFilter.filter,
...(dataViewState.debouncedSearchBar && {
test_case_id: dataViewState.debouncedSearchBar,
}),
},
},
{ query: { placeholderData: keepPreviousData } }
);

Expand All @@ -58,6 +75,17 @@ export const ExperimentSessionsDataView: FC<ExperimentSessionsDataViewProps> = (
[sessionsData]
);

// Client-side status filter for instant feedback while the debounced API request catches up.
const immediateStatusFilter = dataViewState.columnFiltering.state.find((f) => f.id === 'status')
?.value as string | undefined;
const visibleTableData = useMemo(
() =>
immediateStatusFilter
? tableData.filter((row) => row.status === immediateStatusFilter)
: tableData,
[tableData, immediateStatusFilter]
);

// One column per evaluator. The experiment's `evaluator_names` is the
// authoritative, stable set across all sessions; we union in any score keys
// present in the current page's data so a column never goes missing while the
Expand Down Expand Up @@ -130,6 +158,18 @@ export const ExperimentSessionsDataView: FC<ExperimentSessionsDataViewProps> = (
accessor('status', {
header: 'Status',
enableSorting: false,
meta: {
filter: {
type: 'single-select',
label: 'Status',
options: [
{ value: 'success', label: 'Completed' },
{ value: 'error', label: 'Error' },
{ value: 'cancelled', label: 'Cancelled' },
{ value: 'unknown', label: 'Unknown' },
],
},
},
cell: ({ row }) => <StatusBadge status={mapStatusForBadge(row.original.status)} />,
}),
accessor(
Expand Down Expand Up @@ -174,19 +214,56 @@ export const ExperimentSessionsDataView: FC<ExperimentSessionsDataViewProps> = (
<StudioDataView
dataViewState={dataViewState}
makeColumns={makeColumns}
searchField="test_case_id"
toolbarSlotEnd={
<EditColumnsMenu
kind="secondary"
showChevron={false}
slotContent={<div aria-hidden className="h-0 w-[230px]" />}
>
<>
<Columns3 />
<span className="hide-mobile">Columns</span>
</>
</EditColumnsMenu>
}
attributes={{
DataViewRoot: {
data: tableData,
data: visibleTableData,
totalCount,
requestStatus: isLoading && !sessionsData ? 'loading' : undefined,
},
DataViewSearchBar: { placeholder: 'Search case...' },
DataViewTableContent: {
renderEmptyState: () => (
<Empty
experimentGroupName={experimentGroupName}
datasetName={experiment?.dataset_name ?? '<dataset>'}
/>
),
renderEmptyState: () => {
const hasActiveFilters =
!!dataViewState.searchBar.state || dataViewState.columnFiltering.state.length > 0;
if (hasActiveFilters) {
return (
<TableEmptyState
header="No matching test cases"
emptyMessage={
<>
Change your filters and try again, or{' '}
<button
className="text-content-link hover:underline"
onClick={dataViewState.resetFilters}
>
clear filters
</button>
.
</>
}
/>
);
}
return (
<Empty
experimentGroupName={experimentGroupName}
datasetName={experiment?.dataset_name ?? '<dataset>'}
/>
);
},
},
}}
/>
Expand Down
Loading