diff --git a/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx index 981b8792bc..61a05fdb85 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx @@ -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 }; @@ -33,7 +41,7 @@ export const ExperimentSessionsDataView: FC = ( experimentGroupName, }) => { const workspace = useWorkspaceFromPath(); - const dataViewState = useStudioDataViewState({}); + const dataViewState = useStudioDataViewState({ columnVisibility: {} }); const { data: experiment } = useGetExperiment(workspace, experimentName); const page = dataViewState.pagination.state.pageIndex + 1; @@ -42,7 +50,16 @@ export const ExperimentSessionsDataView: FC = ( 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 } } ); @@ -58,6 +75,17 @@ export const ExperimentSessionsDataView: FC = ( [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 @@ -130,6 +158,18 @@ export const ExperimentSessionsDataView: FC = ( 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 }) => , }), accessor( @@ -174,19 +214,56 @@ export const ExperimentSessionsDataView: FC = ( } + > + <> + + Columns + + + } attributes={{ DataViewRoot: { - data: tableData, + data: visibleTableData, totalCount, requestStatus: isLoading && !sessionsData ? 'loading' : undefined, }, + DataViewSearchBar: { placeholder: 'Search case...' }, DataViewTableContent: { - renderEmptyState: () => ( - '} - /> - ), + renderEmptyState: () => { + const hasActiveFilters = + !!dataViewState.searchBar.state || dataViewState.columnFiltering.state.length > 0; + if (hasActiveFilters) { + return ( + + Change your filters and try again, or{' '} + + . + + } + /> + ); + } + return ( + '} + /> + ); + }, }, }} />