From 27d1b0b8fb5ae3829d4955e21a3827fecbd41e71 Mon Sep 17 00:00:00 2001 From: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> Date: Fri, 12 Jun 2026 01:05:39 -0600 Subject: [PATCH 1/5] Experiments UI misc QA Signed-off-by: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> --- .../ExperimentGroupCreateModal/index.tsx | 2 +- .../ExperimentGroupDataView/index.tsx | 103 +++++++++++------- .../ExperimentSessionsDataView/index.tsx | 52 +++++---- .../ExperimentDetailMetrics.tsx | 93 ++++++++++------ .../routes/ExperimentDetailRoute/index.tsx | 2 +- .../ExperimentGroupDetailRoute/index.tsx | 10 +- .../ExperimentRoute/ExperimentGroupCard.tsx | 25 +---- .../src/routes/ExperimentRoute/index.tsx | 2 +- 8 files changed, 171 insertions(+), 118 deletions(-) diff --git a/web/packages/studio/src/components/ExperimentGroupCreateModal/index.tsx b/web/packages/studio/src/components/ExperimentGroupCreateModal/index.tsx index a5a38f5f62..63f6e34a06 100644 --- a/web/packages/studio/src/components/ExperimentGroupCreateModal/index.tsx +++ b/web/packages/studio/src/components/ExperimentGroupCreateModal/index.tsx @@ -130,7 +130,7 @@ export const ExperimentGroupCreateModal: FC = ( > - Create experiment + Create group Coding agent CLI command diff --git a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx index 631913416d..e1584d9751 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx @@ -18,7 +18,7 @@ import type { ExperimentResponse, ListExperimentsSort, } from '@nemo/sdk/generated/platform/schema'; -import { Text, Tooltip } from '@nvidia/foundations-react-core'; +import { Badge, Text, Tooltip } from '@nvidia/foundations-react-core'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getExperimentDetailRoute } from '@studio/routes/utils'; import { tooltipClassName } from '@studio/styles/common'; @@ -118,7 +118,18 @@ export const ExperimentGroupDataView: FC = ({ const { name, summary } = row.original; if (!summary) return {name}; return ( - + + + Summary + + {summary} + + } + className={tooltipClassName} + side="bottom" + > {name} ); @@ -162,7 +173,7 @@ export const ExperimentGroupDataView: FC = ({ }), accessor((original) => original.model_names?.join(', '), { id: 'model_names', - header: 'Model Names', + header: 'Models', enableSorting: false, cell: ({ getValue }) => {getValue() || '-'}, }), @@ -229,43 +240,55 @@ export const ExperimentGroupDataView: FC = ({ return ; } + const serverTotal = experimentsResponse?.pagination?.total_results; + return ( - - navigate(getExperimentDetailRoute(workspace, experimentGroupName, row.name)) - } - toolbarSlotEnd={ - } - > - <> - - Columns - - - } - attributes={{ - DataViewRoot: { - data: tableData, - totalCount, - requestStatus: isGroupLoading || (isLoading && !experimentsData) ? 'loading' : undefined, - }, - DataViewTableContent: { - renderEmptyState: () => ( - - ), - }, - }} - /> +
+
+ Experiments + {serverTotal !== undefined && ( + + {serverTotal} + + )} +
+ + navigate(getExperimentDetailRoute(workspace, experimentGroupName, row.name)) + } + toolbarSlotEnd={ + } + > + <> + + Columns + + + } + attributes={{ + DataViewRoot: { + data: tableData, + totalCount, + requestStatus: isGroupLoading || (isLoading && !experimentsData) ? 'loading' : undefined, + }, + DataViewTableContent: { + renderEmptyState: () => ( + + ), + }, + }} + /> +
); }; diff --git a/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx index 40f610c678..82d9ffd023 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx @@ -8,7 +8,7 @@ import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useGetExperiment, useListExperimentSessions } from '@nemo/sdk/generated/platform/api'; import type { ExperimentSessionResponse } from '@nemo/sdk/generated/platform/schema'; -import { Text, Tooltip } from '@nvidia/foundations-react-core'; +import { Badge, 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'; @@ -162,25 +162,37 @@ export const ExperimentSessionsDataView: FC = ( }), ]; + const serverTotal = sessionsResponse?.pagination?.total_results; + return ( - ( - '} - /> - ), - }, - }} - /> +
+
+ Test cases + {serverTotal !== undefined && ( + + {serverTotal} + + )} +
+ ( + '} + /> + ), + }, + }} + /> +
); }; diff --git a/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx b/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx index f3211635e4..7ea5f84d55 100644 --- a/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx +++ b/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx @@ -4,9 +4,10 @@ import { KVPair } from '@nemo/common/src/components/KVPair'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { useGetExperiment } from '@nemo/sdk/generated/platform/api'; -import { Divider } from '@nvidia/foundations-react-core'; +import { Divider, Text, Tooltip } from '@nvidia/foundations-react-core'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; -import { type FC } from 'react'; +import { tooltipClassName } from '@studio/styles/common'; +import { type FC, type ReactNode } from 'react'; interface ExperimentDetailMetricsProps { experimentName: string; @@ -24,36 +25,66 @@ export const ExperimentDetailMetrics: FC = ({ expe ? `${Math.round(experiment.latency_ms.mean)} ms` : undefined; + const modelNames = experiment?.model_names ?? []; + const modelNamesJoined = modelNames.length > 0 ? modelNames.join(', ') : undefined; + const modelNamesValue: ReactNode = modelNamesJoined ? ( + modelNames.length > 1 ? ( + // Truncate + tooltip for the multi-model case to keep the header KV row compact. + + {modelNamesJoined} + + ) : ( + modelNamesJoined + ) + ) : undefined; + return ( -
- - - : undefined - } - loading={isLoading} - orientation="vertical" - /> - - : undefined - } - loading={isLoading} - orientation="vertical" - /> - - - - +
+
+ + + + + : undefined + } + loading={isLoading} + orientation="vertical" + /> + + : undefined + } + loading={isLoading} + orientation="vertical" + /> +
+
+ + + + + +
); }; diff --git a/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx b/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx index 59aab03136..0a95259ab2 100644 --- a/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx +++ b/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx @@ -21,7 +21,7 @@ export const ExperimentDetailRoute: FC = () => { useBreadcrumbs({ items: [ - { href: getExperimentRoute(workspace), slotLabel: 'Experiments' }, + { href: getExperimentRoute(workspace), slotLabel: 'Experiment Groups' }, { href: getExperimentGroupDetailRoute(workspace, experimentGroupName), slotLabel: experimentGroupName, diff --git a/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx b/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx index fd8fd11445..aa2fb7aeff 100644 --- a/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx +++ b/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { useGetExperimentGroup } from '@nemo/sdk/generated/platform/api'; import { PageHeader, Stack } from '@nvidia/foundations-react-core'; import { AccessibleTitle } from '@studio/components/AccessibleTitle'; import { ExperimentGroupDataView } from '@studio/components/dataViews/ExperimentGroupDataView'; @@ -15,10 +16,11 @@ import { type FC } from 'react'; export const ExperimentGroupDetailRoute: FC = () => { const workspace = useWorkspaceFromPath(); const { experimentGroupName } = useRequiredPathParams([ROUTE_PARAMS.experimentGroupName]); + const { data: group } = useGetExperimentGroup(workspace, experimentGroupName); useBreadcrumbs({ items: [ - { href: getExperimentRoute(workspace), slotLabel: 'Experiments' }, + { href: getExperimentRoute(workspace), slotLabel: 'Experiment Groups' }, { slotLabel: experimentGroupName }, ], }); @@ -26,7 +28,11 @@ export const ExperimentGroupDetailRoute: FC = () => { return ( - + diff --git a/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx b/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx index ed0806a5d6..d06e845c40 100644 --- a/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx +++ b/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx @@ -3,7 +3,7 @@ import { useListExperiments } from '@nemo/sdk/generated/platform/api'; import type { ExperimentGroupResponse } from '@nemo/sdk/generated/platform/schema'; -import { Card, Divider, Text } from '@nvidia/foundations-react-core'; +import { Card, Text } from '@nvidia/foundations-react-core'; import { Metric } from '@studio/routes/ExperimentRoute/Metric'; import { UpdatedAt } from '@studio/routes/ExperimentRoute/UpdatedAt'; import { getExperimentGroupDetailRoute } from '@studio/routes/utils'; @@ -20,25 +20,10 @@ export const ExperimentGroupCard: FC = ({ group, works const { data: experimentsData } = useListExperiments(workspace, { filter: { experiment_group_id: group.id }, - page_size: 100, + page_size: 1, }); - const experiments = experimentsData?.data ?? []; - const experimentCount = experimentsData?.pagination?.total_results ?? experiments.length; - - // Collect evaluator names and average their means across experiments in this group - const evaluatorNames = [ - ...new Set(experiments.flatMap((e) => Object.keys(e.aggregate_scores ?? {}))), - ]; - const scoreEntries = evaluatorNames - .map((name) => { - const means = experiments - .map((e) => e.aggregate_scores?.[name]?.mean) - .filter((v): v is number => v !== undefined && v !== null); - const avg = means.length > 0 ? means.reduce((a, b) => a + b, 0) / means.length : null; - return { name, avg }; - }) - .filter((entry): entry is { name: string; avg: number } => entry.avg !== null); + const experimentCount = experimentsData?.pagination?.total_results ?? 0; return ( = ({ group, works {/* Stats */}
- {scoreEntries.map(({ name, avg }) => ( - - ))} - {scoreEntries.length > 0 && }
diff --git a/web/packages/studio/src/routes/ExperimentRoute/index.tsx b/web/packages/studio/src/routes/ExperimentRoute/index.tsx index f32ea4bf07..27cb53ea35 100644 --- a/web/packages/studio/src/routes/ExperimentRoute/index.tsx +++ b/web/packages/studio/src/routes/ExperimentRoute/index.tsx @@ -31,7 +31,7 @@ import { type FC, useState } from 'react'; const DEFAULT_PAGE_SIZE = 5; export const ExperimentRoute: FC = () => { - useBreadcrumbs({ items: [{ slotLabel: 'Experiments' }] }); + useBreadcrumbs({ items: [{ slotLabel: 'Experiment Groups' }] }); const workspace = useWorkspaceFromPath(); const [page, setPage] = useState(1); From 5dae6098fe1aa64033b9deeedb7b62371ce9b395 Mon Sep 17 00:00:00 2001 From: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> Date: Fri, 12 Jun 2026 01:12:18 -0600 Subject: [PATCH 2/5] pretty Signed-off-by: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> --- .../components/dataViews/ExperimentGroupDataView/index.tsx | 3 ++- .../ExperimentDetailRoute/ExperimentDetailMetrics.tsx | 7 +------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx index e1584d9751..38a0b5aacd 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx @@ -277,7 +277,8 @@ export const ExperimentGroupDataView: FC = ({ DataViewRoot: { data: tableData, totalCount, - requestStatus: isGroupLoading || (isLoading && !experimentsData) ? 'loading' : undefined, + requestStatus: + isGroupLoading || (isLoading && !experimentsData) ? 'loading' : undefined, }, DataViewTableContent: { renderEmptyState: () => ( diff --git a/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx b/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx index 7ea5f84d55..028c53a250 100644 --- a/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx +++ b/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx @@ -74,12 +74,7 @@ export const ExperimentDetailMetrics: FC = ({ expe />
- + From 045e622fb5011e4a297fd5b8f34668ce07a6b949 Mon Sep 17 00:00:00 2001 From: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:20:16 -0600 Subject: [PATCH 3/5] pr comments (pre sdk-update) Signed-off-by: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> --- openapi/ga/individual/platform.openapi.yaml | 5 ++ openapi/ga/openapi.yaml | 5 ++ openapi/openapi.yaml | 5 ++ .../intake/api/v2/experiments/endpoints.py | 47 +++++++++- .../nmp/intake/api/v2/experiments/schemas.py | 4 + .../ExperimentGroupDataView/index.tsx | 89 ++++++++----------- .../ExperimentSessionsDataView/index.tsx | 52 +++++------ .../ExperimentDetailMetrics.tsx | 14 +-- .../routes/ExperimentDetailRoute/index.tsx | 24 +++-- .../ExperimentGroupDetailRoute/index.tsx | 14 ++- .../ExperimentRoute/ExperimentGroupCard.tsx | 10 +-- 11 files changed, 160 insertions(+), 109 deletions(-) diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index dba0e62e51..a3bce5107d 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -10024,6 +10024,11 @@ components: title: Updated At type: string format: date-time + experiment_count: + type: integer + title: Experiment Count + description: Number of live (non-soft-deleted) experiments in this group. + default: 0 type: object required: - id diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index dba0e62e51..a3bce5107d 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -10024,6 +10024,11 @@ components: title: Updated At type: string format: date-time + experiment_count: + type: integer + title: Experiment Count + description: Number of live (non-soft-deleted) experiments in this group. + default: 0 type: object required: - id diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index dba0e62e51..a3bce5107d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -10024,6 +10024,11 @@ components: title: Updated At type: string format: date-time + experiment_count: + type: integer + title: Experiment Count + description: Number of live (non-soft-deleted) experiments in this group. + default: 0 type: object required: - id diff --git a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py index 3e2513aa53..1fd26c2245 100644 --- a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py +++ b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py @@ -11,6 +11,7 @@ from __future__ import annotations +import asyncio import logging import secrets import time @@ -145,8 +146,17 @@ async def list_experiment_groups( page=page, page_size=page_size, ) + responses = [ExperimentGroupResponse.from_entity(e) for e in result.data] + counts = await asyncio.gather( + *[ + _count_live_experiments_in_group(entity_client, workspace=workspace, group_id=g.id) + for g in result.data + ] + ) + for response, count in zip(responses, counts): + response.experiment_count = count return Page( - data=[ExperimentGroupResponse.from_entity(e) for e in result.data], + data=responses, pagination=PaginationData(**result.pagination.model_dump()), sort=sort, filter=parsed.to_response(), @@ -172,7 +182,11 @@ async def get_experiment_group( label="Experiment group", ) _reject_if_deleted(entity, workspace=workspace, name=name, label="Experiment group") - return ExperimentGroupResponse.from_entity(entity) + response = ExperimentGroupResponse.from_entity(entity) + response.experiment_count = await _count_live_experiments_in_group( + entity_client, workspace=workspace, group_id=entity.id + ) + return response @router.put( @@ -602,6 +616,35 @@ async def _soft_delete(entity_client: EntityClient, entity: Experiment | Experim await entity_client.update(entity, original_name=original_name) +async def _count_live_experiments_in_group( + entity_client: EntityClient, *, workspace: str, group_id: str +) -> int: + """Return the number of non-soft-deleted experiments in a group. + + Fetches via ``list(page_size=1)`` so the response carries only ``pagination.total_results`` — + cheap enough to fan out per-group via ``asyncio.gather`` on the list endpoint. + """ + result = await entity_client.list( + Experiment, + workspace=workspace, + filter_operation=LogicalOperation( + operator=FilterOperator.AND, + operations=[ + ComparisonOperation(operator=FilterOperator.EQ, field="data.experiment_group_id", value=group_id), + LogicalOperation( + operator=FilterOperator.NOT, + operations=[ + ComparisonOperation(operator=FilterOperator.EQ, field="data.is_deleted", value=True), + ], + ), + ], + ), + page=1, + page_size=1, + ) + return result.pagination.total_results + + async def _validate_group_exists(entity_client: EntityClient, *, group_id: str) -> None: """Reject the request with 400 if the referenced ExperimentGroup doesn't exist or is deleted.""" try: diff --git a/services/intake/src/nmp/intake/api/v2/experiments/schemas.py b/services/intake/src/nmp/intake/api/v2/experiments/schemas.py index 06d3beff2d..6870107ea6 100644 --- a/services/intake/src/nmp/intake/api/v2/experiments/schemas.py +++ b/services/intake/src/nmp/intake/api/v2/experiments/schemas.py @@ -56,6 +56,10 @@ class ExperimentGroupResponse(BaseModel): description: str | None = None created_at: datetime | None = None updated_at: datetime | None = None + experiment_count: int = Field( + default=0, + description="Number of live (non-soft-deleted) experiments in this group.", + ) @classmethod def from_entity(cls, entity: ExperimentGroup) -> ExperimentGroupResponse: diff --git a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx index 38a0b5aacd..b59143008f 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx @@ -18,7 +18,7 @@ import type { ExperimentResponse, ListExperimentsSort, } from '@nemo/sdk/generated/platform/schema'; -import { Badge, Text, Tooltip } from '@nvidia/foundations-react-core'; +import { Text, Tooltip } from '@nvidia/foundations-react-core'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getExperimentDetailRoute } from '@studio/routes/utils'; import { tooltipClassName } from '@studio/styles/common'; @@ -240,56 +240,43 @@ export const ExperimentGroupDataView: FC = ({ return ; } - const serverTotal = experimentsResponse?.pagination?.total_results; - return ( -
-
- Experiments - {serverTotal !== undefined && ( - - {serverTotal} - - )} -
- - navigate(getExperimentDetailRoute(workspace, experimentGroupName, row.name)) - } - toolbarSlotEnd={ - } - > - <> - - Columns - - - } - attributes={{ - DataViewRoot: { - data: tableData, - totalCount, - requestStatus: - isGroupLoading || (isLoading && !experimentsData) ? 'loading' : undefined, - }, - DataViewTableContent: { - renderEmptyState: () => ( - - ), - }, - }} - /> -
+ + navigate(getExperimentDetailRoute(workspace, experimentGroupName, row.name)) + } + toolbarSlotEnd={ + } + > + <> + + Columns + + + } + attributes={{ + DataViewRoot: { + data: tableData, + totalCount, + requestStatus: isGroupLoading || (isLoading && !experimentsData) ? 'loading' : undefined, + }, + DataViewTableContent: { + renderEmptyState: () => ( + + ), + }, + }} + /> ); }; diff --git a/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx index 82d9ffd023..40f610c678 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentSessionsDataView/index.tsx @@ -8,7 +8,7 @@ import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useGetExperiment, useListExperimentSessions } from '@nemo/sdk/generated/platform/api'; import type { ExperimentSessionResponse } from '@nemo/sdk/generated/platform/schema'; -import { Badge, Text, Tooltip } from '@nvidia/foundations-react-core'; +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'; @@ -162,37 +162,25 @@ export const ExperimentSessionsDataView: FC = ( }), ]; - const serverTotal = sessionsResponse?.pagination?.total_results; - return ( -
-
- Test cases - {serverTotal !== undefined && ( - - {serverTotal} - - )} -
- ( - '} - /> - ), - }, - }} - /> -
+ ( + '} + /> + ), + }, + }} + /> ); }; diff --git a/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx b/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx index 028c53a250..0c97a8ad15 100644 --- a/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx +++ b/web/packages/studio/src/routes/ExperimentDetailRoute/ExperimentDetailMetrics.tsx @@ -4,7 +4,7 @@ import { KVPair } from '@nemo/common/src/components/KVPair'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { useGetExperiment } from '@nemo/sdk/generated/platform/api'; -import { Divider, Text, Tooltip } from '@nvidia/foundations-react-core'; +import { Divider, Flex, Text, Tooltip } from '@nvidia/foundations-react-core'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { tooltipClassName } from '@studio/styles/common'; import { type FC, type ReactNode } from 'react'; @@ -39,8 +39,8 @@ export const ExperimentDetailMetrics: FC = ({ expe ) : undefined; return ( -
-
+ + = ({ expe loading={isLoading} orientation="vertical" /> -
-
+ + -
-
+ + ); }; diff --git a/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx b/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx index 0a95259ab2..7fa666471e 100644 --- a/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx +++ b/web/packages/studio/src/routes/ExperimentDetailRoute/index.tsx @@ -1,14 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { PageHeader, Stack } from '@nvidia/foundations-react-core'; +import { useGetExperiment } from '@nemo/sdk/generated/platform/api'; +import { Badge, PageHeader, Stack, Text } from '@nvidia/foundations-react-core'; import { AccessibleTitle } from '@studio/components/AccessibleTitle'; import { ExperimentSessionsDataView } from '@studio/components/dataViews/ExperimentSessionsDataView'; import { ROUTE_PARAMS } from '@studio/constants/routes'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; import { ExperimentDetailMetrics } from '@studio/routes/ExperimentDetailRoute/ExperimentDetailMetrics'; -import { getExperimentRoute, getExperimentGroupDetailRoute } from '@studio/routes/utils'; +import { getExperimentGroupDetailRoute, getExperimentRoute } from '@studio/routes/utils'; import { useRequiredPathParams } from '@studio/util/hooks/useRequiredPathParams'; import { type FC } from 'react'; @@ -18,6 +19,7 @@ export const ExperimentDetailRoute: FC = () => { ROUTE_PARAMS.experimentGroupName, ROUTE_PARAMS.experimentName, ]); + const { data: experiment } = useGetExperiment(workspace, experimentName); useBreadcrumbs({ items: [ @@ -35,10 +37,20 @@ export const ExperimentDetailRoute: FC = () => { - +
+
+ Test cases + {experiment?.run_count !== undefined && ( + + {experiment.run_count} + + )} +
+ +
); diff --git a/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx b/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx index aa2fb7aeff..b6de6e92aa 100644 --- a/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx +++ b/web/packages/studio/src/routes/ExperimentGroupDetailRoute/index.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useGetExperimentGroup } from '@nemo/sdk/generated/platform/api'; -import { PageHeader, Stack } from '@nvidia/foundations-react-core'; +import { Badge, PageHeader, Stack, Text } from '@nvidia/foundations-react-core'; import { AccessibleTitle } from '@studio/components/AccessibleTitle'; import { ExperimentGroupDataView } from '@studio/components/dataViews/ExperimentGroupDataView'; import { ROUTE_PARAMS } from '@studio/constants/routes'; @@ -34,7 +34,17 @@ export const ExperimentGroupDetailRoute: FC = () => { slotDescription={group?.description || undefined} /> - +
+
+ Experiments + {group?.experiment_count !== undefined && ( + + {group.experiment_count} + + )} +
+ +
); diff --git a/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx b/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx index d06e845c40..040beb1011 100644 --- a/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx +++ b/web/packages/studio/src/routes/ExperimentRoute/ExperimentGroupCard.tsx @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { useListExperiments } from '@nemo/sdk/generated/platform/api'; import type { ExperimentGroupResponse } from '@nemo/sdk/generated/platform/schema'; import { Card, Text } from '@nvidia/foundations-react-core'; import { Metric } from '@studio/routes/ExperimentRoute/Metric'; @@ -18,13 +17,6 @@ interface ExperimentGroupCardProps { export const ExperimentGroupCard: FC = ({ group, workspace }) => { const navigate = useNavigate(); - const { data: experimentsData } = useListExperiments(workspace, { - filter: { experiment_group_id: group.id }, - page_size: 1, - }); - - const experimentCount = experimentsData?.pagination?.total_results ?? 0; - return ( = ({ group, works {/* Stats */}
- +
); From ad571ad8db7e7a7a8869d3bba85d5ab823f3d2b4 Mon Sep 17 00:00:00 2001 From: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:24:23 -0600 Subject: [PATCH 4/5] update sdk Signed-off-by: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> --- sdk/python/nemo-platform/.nmpcontext/openapi.yaml | 5 +++++ .../types/experiment_groups/experiment_group_response.py | 3 +++ .../src/nmp/intake/api/v2/experiments/endpoints.py | 9 ++------- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 386598e5cf..d42441386b 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -10007,6 +10007,11 @@ components: title: Updated At type: string format: date-time + experiment_count: + type: integer + title: Experiment Count + description: Number of live (non-soft-deleted) experiments in this group. + default: 0 type: object required: - id diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/experiment_groups/experiment_group_response.py b/sdk/python/nemo-platform/src/nemo_platform/types/experiment_groups/experiment_group_response.py index 2a8aa6ed06..04e48f3069 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/experiment_groups/experiment_group_response.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/experiment_groups/experiment_group_response.py @@ -36,4 +36,7 @@ class ExperimentGroupResponse(BaseModel): description: Optional[str] = None + experiment_count: Optional[int] = None + """Number of live (non-soft-deleted) experiments in this group.""" + updated_at: Optional[datetime] = None diff --git a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py index 44487848a3..4e31921f73 100644 --- a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py +++ b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py @@ -148,10 +148,7 @@ async def list_experiment_groups( ) responses = [ExperimentGroupResponse.from_entity(e) for e in result.data] counts = await asyncio.gather( - *[ - _count_live_experiments_in_group(entity_client, workspace=workspace, group_id=g.id) - for g in result.data - ] + *[_count_live_experiments_in_group(entity_client, workspace=workspace, group_id=g.id) for g in result.data] ) for response, count in zip(responses, counts): response.experiment_count = count @@ -615,9 +612,7 @@ async def _soft_delete(entity_client: EntityClient, entity: Experiment | Experim await entity_client.update(entity, original_name=original_name) -async def _count_live_experiments_in_group( - entity_client: EntityClient, *, workspace: str, group_id: str -) -> int: +async def _count_live_experiments_in_group(entity_client: EntityClient, *, workspace: str, group_id: str) -> int: """Return the number of non-soft-deleted experiments in a group. Fetches via ``list(page_size=1)`` so the response carries only ``pagination.total_results`` — From 7c28b325bc582da5be9b34b37e3838fbd16a6989 Mon Sep 17 00:00:00 2001 From: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:31:46 -0600 Subject: [PATCH 5/5] coderabbit Signed-off-by: shanaiabuggy <59746633+shanaiabuggy@users.noreply.github.com> --- .../src/nmp/intake/api/v2/experiments/endpoints.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py index 4e31921f73..1379462b9a 100644 --- a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py +++ b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py @@ -147,6 +147,9 @@ async def list_experiment_groups( page_size=page_size, ) responses = [ExperimentGroupResponse.from_entity(e) for e in result.data] + # One count query per group, fanned out in parallel. Linear in the page size (up to 1000) + # but parallelized, so wall-clock stays low. If group counts become a hot path, replace + # with a bulk aggregate on the entity store rather than scaling this gather wider. counts = await asyncio.gather( *[_count_live_experiments_in_group(entity_client, workspace=workspace, group_id=g.id) for g in result.data] ) @@ -216,7 +219,11 @@ async def update_experiment_group( ) existing.description = body.description updated = await entity_client.update(existing) - return ExperimentGroupResponse.from_entity(updated) + response = ExperimentGroupResponse.from_entity(updated) + response.experiment_count = await _count_live_experiments_in_group( + entity_client, workspace=workspace, group_id=updated.id + ) + return response @router.delete(