diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index c47288d6ef..e87ddb22c9 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -3772,7 +3772,7 @@ nemo jobs list [OPTIONS] * `--workspace` * `--page `: Page number. * `--page-size `: Page size. -* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, updated_at, -updated_at] +* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, updated_at, -updated_at, source, -source] * `--all-pages`: Fetch all pages **Filter Options:** diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index fd1e06dbb6..3c24a300db 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -4583,7 +4583,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/PlatformJobSortField' + - $ref: '#/components/schemas/PlatformJobListSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: -created_at @@ -14969,6 +14969,17 @@ components: required: - data title: PlatformJobListResultResponse + PlatformJobListSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + - source + - -source + title: PlatformJobListSortField + description: Sort fields for the job *list* endpoint. PlatformJobListTaskResponse: properties: data: diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index fd1e06dbb6..3c24a300db 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -4583,7 +4583,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/PlatformJobSortField' + - $ref: '#/components/schemas/PlatformJobListSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: -created_at @@ -14969,6 +14969,17 @@ components: required: - data title: PlatformJobListResultResponse + PlatformJobListSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + - source + - -source + title: PlatformJobListSortField + description: Sort fields for the job *list* endpoint. PlatformJobListTaskResponse: properties: data: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index fd1e06dbb6..3c24a300db 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -4583,7 +4583,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/PlatformJobSortField' + - $ref: '#/components/schemas/PlatformJobListSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: -created_at @@ -14969,6 +14969,17 @@ components: required: - data title: PlatformJobListResultResponse + PlatformJobListSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + - source + - -source + title: PlatformJobListSortField + description: Sort fields for the job *list* endpoint. PlatformJobListTaskResponse: properties: data: diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/jobs/__init__.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/jobs/__init__.py index 29a9636df8..f47e9ccc80 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/jobs/__init__.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/jobs/__init__.py @@ -307,7 +307,7 @@ def list_jobs( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "source", "-source"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py index 0ab3757f88..6b9a9a80bb 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/types.py @@ -87,6 +87,25 @@ def get_sort_direction(self) -> str: return "desc" if self.value.startswith("-") else "asc" +class PlatformJobListSortField(str, Enum): + """Sort fields for the job *list* endpoint.""" + + # Superset of PlatformJobSortField with `source`; only the job list can sort + # by source (steps/results/logs have no source field). + CREATED_AT_ASC = "created_at" + CREATED_AT_DESC = "-created_at" + UPDATED_AT_ASC = "updated_at" + UPDATED_AT_DESC = "-updated_at" + SOURCE_ASC = "source" + SOURCE_DESC = "-source" + + def get_field_name(self) -> str: + return self.value.lstrip("-") + + def get_sort_direction(self) -> str: + return "desc" if self.value.startswith("-") else "asc" + + class PlatformJobAttemptSortField(str, Enum): SEQ_ASC = "seq" SEQ_DESC = "-seq" diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index fd1e06dbb6..d14fb8df88 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -4583,7 +4583,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/PlatformJobSortField' + - $ref: '#/components/schemas/PlatformJobListSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: -created_at @@ -14969,6 +14969,17 @@ components: required: - data title: PlatformJobListResultResponse + PlatformJobListSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + - source + - -source + title: PlatformJobListSortField + description: 'Sort fields for the job *list* endpoint.' PlatformJobListTaskResponse: properties: data: diff --git a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml index 5bddc29758..8506cbce07 100644 --- a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml @@ -550,6 +550,7 @@ resources: kubernetes_volume: KubernetesVolume kubernetes_volume_mount: KubernetesVolumeMount platform_job_environment_variable: PlatformJobEnvironmentVariable + platform_job_list_sort_field: PlatformJobListSortField platform_job_response: PlatformJobResponse platform_job_responses_page: PlatformJobResponsesPage platform_job_secret_environment_variable_ref: PlatformJobSecretEnvironmentVariableRef @@ -915,11 +916,11 @@ resources: evaluations: standalone_api: true models: - evaluator_aggregate: EvaluatorAggregate evaluation_filter: EvaluationFilter evaluation_request: EvaluationRequest evaluation_response: EvaluationResponse evaluation_responses_page: EvaluationResponsesPage + evaluator_aggregate: EvaluatorAggregate metric_stat_filters: MetricStatFilters number_filter: NumberFilter methods: diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/jobs/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/jobs/__init__.py index 9e2af86423..2dfd265a48 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/jobs/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/jobs/__init__.py @@ -307,7 +307,7 @@ def list_jobs( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "source", "-source"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md index f5bfd41e0c..c06db14788 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md @@ -31,6 +31,7 @@ from nemo_platform.types.jobs import ( KubernetesVolume, KubernetesVolumeMount, PlatformJobEnvironmentVariable, + PlatformJobListSortField, PlatformJobResponse, PlatformJobResponsesPage, PlatformJobSecretEnvironmentVariableRef, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py index 9c5652b216..63df9bd715 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py @@ -57,8 +57,8 @@ ) from ...pagination import SyncLogsPagination, AsyncLogsPagination, SyncDefaultPagination, AsyncDefaultPagination from ...types.jobs import ( - PlatformJobSortField, PlatformJobSpecParam, + PlatformJobListSortField, job_list_params, job_create_params, job_get_logs_params, @@ -67,8 +67,8 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.shared.platform_job_log import PlatformJobLog from ...types.jobs.platform_job_response import PlatformJobResponse -from ...types.jobs.platform_job_sort_field import PlatformJobSortField from ...types.jobs.platform_job_spec_param import PlatformJobSpecParam +from ...types.jobs.platform_job_list_sort_field import PlatformJobListSortField from ...types.shared.platform_job_status_response import PlatformJobStatusResponse from ...types.jobs.platform_jobs_list_filter_param import PlatformJobsListFilterParam from ...types.jobs.job_list_execution_profiles_response import JobListExecutionProfilesResponse @@ -211,7 +211,7 @@ def list( filter: PlatformJobsListFilterParam | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: PlatformJobSortField | Omit = omit, + sort: PlatformJobListSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -721,7 +721,7 @@ def list( filter: PlatformJobsListFilterParam | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: PlatformJobSortField | Omit = omit, + sort: PlatformJobListSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/jobs/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/jobs/__init__.py index 6c6a8d01c1..792e99704e 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/jobs/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/jobs/__init__.py @@ -55,6 +55,7 @@ from .docker_job_execution_profile import DockerJobExecutionProfile as DockerJobExecutionProfile from .gpu_execution_provider_param import GPUExecutionProviderParam as GPUExecutionProviderParam from .job_execution_profile_config import JobExecutionProfileConfig as JobExecutionProfileConfig +from .platform_job_list_sort_field import PlatformJobListSortField as PlatformJobListSortField from .platform_job_step_spec_param import PlatformJobStepSpecParam as PlatformJobStepSpecParam from .task_create_or_update_params import TaskCreateOrUpdateParams as TaskCreateOrUpdateParams from .kubernetes_job_storage_config import KubernetesJobStorageConfig as KubernetesJobStorageConfig diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/jobs/job_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/jobs/job_list_params.py index 754bbf523d..57a754aa89 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/jobs/job_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/jobs/job_list_params.py @@ -19,7 +19,7 @@ from typing_extensions import TypedDict -from .platform_job_sort_field import PlatformJobSortField +from .platform_job_list_sort_field import PlatformJobListSortField from .platform_jobs_list_filter_param import PlatformJobsListFilterParam __all__ = ["JobListParams"] @@ -40,7 +40,7 @@ class JobListParams(TypedDict, total=False): page_size: int """Page size.""" - sort: PlatformJobSortField + sort: PlatformJobListSortField """The field to sort by. To sort in decreasing order, use `-` in front of the field name. diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/jobs/platform_job_list_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/jobs/platform_job_list_sort_field.py new file mode 100644 index 0000000000..206700be66 --- /dev/null +++ b/sdk/python/nemo-platform/src/nemo_platform/types/jobs/platform_job_list_sort_field.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal, TypeAlias + +__all__ = ["PlatformJobListSortField"] + +PlatformJobListSortField: TypeAlias = Literal[ + "created_at", "-created_at", "updated_at", "-updated_at", "source", "-source" +] diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index 5bddc29758..8506cbce07 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -550,6 +550,7 @@ resources: kubernetes_volume: KubernetesVolume kubernetes_volume_mount: KubernetesVolumeMount platform_job_environment_variable: PlatformJobEnvironmentVariable + platform_job_list_sort_field: PlatformJobListSortField platform_job_response: PlatformJobResponse platform_job_responses_page: PlatformJobResponsesPage platform_job_secret_environment_variable_ref: PlatformJobSecretEnvironmentVariableRef @@ -915,11 +916,11 @@ resources: evaluations: standalone_api: true models: - evaluator_aggregate: EvaluatorAggregate evaluation_filter: EvaluationFilter evaluation_request: EvaluationRequest evaluation_response: EvaluationResponse evaluation_responses_page: EvaluationResponsesPage + evaluator_aggregate: EvaluatorAggregate metric_stat_filters: MetricStatFilters number_filter: NumberFilter methods: diff --git a/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/endpoints.py b/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/endpoints.py index ef3b5eba2b..92f3c0c233 100644 --- a/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/endpoints.py +++ b/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/endpoints.py @@ -33,6 +33,7 @@ from nmp.core.jobs.api.dependencies import dep_dispatcher from nmp.core.jobs.api.v2.jobs.schemas import ( CreatePlatformJobRequest, + PlatformJobListSortField, PlatformJobListTaskResponse, PlatformJobResponse, PlatformJobsListFilter, @@ -182,8 +183,8 @@ async def list_jobs( workspace: str, page: int = Query(default=1, description="Page number.", gt=0), page_size: int = Query(default=10, description="Page size.", gt=0), - sort: PlatformJobSortField = Query( - default=PlatformJobSortField.CREATED_AT_DESC, + sort: PlatformJobListSortField = Query( + default=PlatformJobListSortField.CREATED_AT_DESC, description="The field to sort by. To sort in decreasing order, use `-` in front of the field name.", ), parsed: ParsedFilter = Depends(make_filter_dep(PlatformJobsListFilter)), diff --git a/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/schemas.py b/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/schemas.py index 31ca0b1978..bdde92aa46 100644 --- a/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/schemas.py +++ b/services/core/jobs/src/nmp/core/jobs/api/v2/jobs/schemas.py @@ -36,6 +36,7 @@ CreatePlatformJobRequest = _types.CreatePlatformJobRequest PlatformJobAttemptSortField = _types.PlatformJobAttemptSortField PlatformJobLogSortField = _types.PlatformJobLogSortField +PlatformJobListSortField = _types.PlatformJobListSortField PlatformJobResponse = _types.PlatformJobResponse PlatformJobSortField = _types.PlatformJobSortField PlatformJobStatusDetailsUpdateRequest = _types.PlatformJobStatusDetailsUpdateRequest diff --git a/services/core/jobs/src/nmp/core/jobs/app/dispatcher.py b/services/core/jobs/src/nmp/core/jobs/app/dispatcher.py index 907494af4f..fd12f0f918 100644 --- a/services/core/jobs/src/nmp/core/jobs/app/dispatcher.py +++ b/services/core/jobs/src/nmp/core/jobs/app/dispatcher.py @@ -28,6 +28,7 @@ from nmp.common.sdk_factory import get_entity_parts from nmp.core.jobs.api.v2.jobs.schemas import ( CreatePlatformJobRequest, + PlatformJobListSortField, PlatformJobResponse, PlatformJobSortField, PlatformJobStepsListFilter, @@ -339,7 +340,7 @@ async def list_jobs( workspace: str, limit: Optional[int] = None, offset: Optional[int] = None, - sort: Optional[PlatformJobSortField] = None, + sort: Optional[PlatformJobListSortField] = None, ) -> Tuple[List[PlatformJobResponse], int]: """List platform jobs with their current attempts.""" # Status lives on PlatformJobAttempt, not PlatformJob, so the full filter diff --git a/services/core/jobs/tests/test_dispatcher.py b/services/core/jobs/tests/test_dispatcher.py index 0e46800685..fd411f6b58 100644 --- a/services/core/jobs/tests/test_dispatcher.py +++ b/services/core/jobs/tests/test_dispatcher.py @@ -808,7 +808,7 @@ async def test_list_jobs_across_multiple_workspaces( from unittest.mock import MagicMock from nmp.common.entities.client import EntityClient - from nmp.core.jobs.api.v2.jobs.schemas import PlatformJobSortField + from nmp.core.jobs.api.v2.jobs.schemas import PlatformJobListSortField from nmp.testing import create_test_client # Create entity store with multiple workspaces and projects @@ -870,7 +870,7 @@ async def test_list_jobs_across_multiple_workspaces( # List jobs in "default" workspace jobs_default, count_default = await mock_dispatcher.list_jobs( parsed=ParsedFilter(operation=None), - sort=PlatformJobSortField.CREATED_AT_ASC, + sort=PlatformJobListSortField.CREATED_AT_ASC, limit=100, offset=0, workspace=DEFAULT_WORKSPACE, @@ -892,7 +892,7 @@ async def test_list_jobs_across_multiple_workspaces( # List jobs in "other-workspace" jobs_other, count_other = await mock_dispatcher.list_jobs( parsed=ParsedFilter(operation=None), - sort=PlatformJobSortField.CREATED_AT_ASC, + sort=PlatformJobListSortField.CREATED_AT_ASC, limit=100, offset=0, workspace="other-workspace", @@ -912,7 +912,7 @@ async def test_list_jobs_across_multiple_workspaces( # Now query across both workspaces using ALL_WORKSPACES jobs_all, count_all = await mock_dispatcher.list_jobs( parsed=ParsedFilter(operation=None), - sort=PlatformJobSortField.CREATED_AT_ASC, + sort=PlatformJobListSortField.CREATED_AT_ASC, limit=100, offset=0, workspace=ALL_WORKSPACES, @@ -923,6 +923,41 @@ async def test_list_jobs_across_multiple_workspaces( assert count_all == 4 +@pytest.mark.asyncio +async def test_list_jobs_sort_by_source( + mock_dispatcher: JobDispatcher, + mock_store: EntityClient, +): + """list_jobs sorts by the source field ascending and descending.""" + from nmp.core.jobs.api.v2.jobs.schemas import PlatformJobListSortField + + for source in ("zebra-source", "alpha-source", "middle-source"): + await mock_dispatcher.create_job( + CreatePlatformJobRequest( + name=f"job-{source}", + source=source, + project=TestConstants.PROJECT, + spec=TestConstants.SPEC_BASIC, + platform_spec=TestConstants.PLATFORM_SPEC, + ), + DEFAULT_WORKSPACE, + ) + + jobs_asc, _ = await mock_dispatcher.list_jobs( + parsed=ParsedFilter(operation=None), + sort=PlatformJobListSortField.SOURCE_ASC, + workspace=DEFAULT_WORKSPACE, + ) + assert [j.source for j in jobs_asc] == ["alpha-source", "middle-source", "zebra-source"] + + jobs_desc, _ = await mock_dispatcher.list_jobs( + parsed=ParsedFilter(operation=None), + sort=PlatformJobListSortField.SOURCE_DESC, + workspace=DEFAULT_WORKSPACE, + ) + assert [j.source for j in jobs_desc] == ["zebra-source", "middle-source", "alpha-source"] + + # ============================================================================= # list_jobs: status in-memory filtering via ParsedFilter # ============================================================================= diff --git a/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx index 3f9dd69ac3..6fb195f048 100644 --- a/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx @@ -13,6 +13,7 @@ import { workspace1 } from '@studio/mocks/entity-store/projects'; import { server } from '@studio/mocks/node'; import { getWorkspaceJobsRoute } from '@studio/routes/utils'; import { renderRoute, screen, waitFor } from '@studio/tests/util/render'; +import { fireEvent } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; vi.mock('use-debounce', () => ({ @@ -107,6 +108,21 @@ describe('JobsDataView', () => { } }); + it('adds a returned plugin source to the source filter options', async () => { + const jobs = [ + makeJob({ name: 'agents-run-1', source: 'nemo-agents-plugin' }), + makeJob({ name: 'eval-run-2', source: 'evaluator-metrics', id: 'job-id-2' }), + ]; + server.use(http.get(JOBS_URL, () => HttpResponse.json(makeJobsPage(jobs)))); + + renderComponent(); + + fireEvent.click(await screen.findByTestId('open-filters-button')); + fireEvent.click(await screen.findByTestId('column-filter-source')); + + expect(await screen.findByRole('option', { name: 'nemo-agents-plugin' })).toBeInTheDocument(); + }); + it('shows error panel when API returns an error', async () => { server.use(http.get(JOBS_URL, () => HttpResponse.error())); @@ -211,5 +227,22 @@ describe('JobsDataView', () => { expect(Array.from(params.keys()).some((k) => k.startsWith('search['))).toBe(false); } }); + + it('sends sort=source when the Source header is clicked', async () => { + const requestUrls: string[] = []; + server.use( + http.get(JOBS_URL, ({ request }) => { + requestUrls.push(request.url); + return HttpResponse.json(makeJobsPage([makeJob()])); + }) + ); + renderComponent(); + + fireEvent.click(await screen.findByRole('button', { name: /^Source/ })); + + await waitFor(() => + expect(requestUrls.some((u) => new URL(u).searchParams.get('sort') === 'source')).toBe(true) + ); + }); }); }); diff --git a/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx index 2a220cf867..8799533bea 100644 --- a/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx @@ -11,8 +11,8 @@ import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataView import { getSortParam } from '@nemo/common/src/utils/query'; import { useJobsListJobs } from '@nemo/sdk/generated/platform/api'; import type { + PlatformJobListSortField, PlatformJobResponse, - PlatformJobSortField, PlatformJobsListFilter, } from '@nemo/sdk/generated/platform/schema'; import { Button, Flex, StatusMessage } from '@nvidia/foundations-react-core'; @@ -32,7 +32,7 @@ import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { iconColorClass } from '@studio/routes/constants'; import { keepPreviousData } from '@tanstack/react-query'; import { ChartBar, Cog, LayoutList, ListChecks, Sliders, Sparkles } from 'lucide-react'; -import { ComponentProps, type ReactNode } from 'react'; +import { ComponentProps, type ReactNode, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; const SOURCE_DISPLAY: Record = { @@ -86,7 +86,7 @@ export const JobsDataView = () => { { page: dataViewState.pagination.state.pageIndex + 1, page_size: dataViewState.pagination.state.pageSize, - sort: getSortParam(dataViewState.sorting.state) as PlatformJobSortField, + sort: getSortParam(dataViewState.sorting.state) as PlatformJobListSortField, filter: { ...userFilter, ...(hasUserSourceFilter @@ -113,6 +113,19 @@ export const JobsDataView = () => { ? (jobsData?.data ?? []) : jobsData.data.filter((job) => job.source !== JOB_SOURCE.CUSTOMIZATION); + // Surface plugin sources not in the static list (e.g. nemo-agents-plugin). + const seenSourcesRef = useRef>(new Set()); + for (const job of jobsData?.data ?? []) { + if (job.source) seenSourcesRef.current.add(job.source); + } + + const staticSourceValues = new Set(sourceFilterOptions.map((option) => option.value)); + const dynamicSourceOptions = [...seenSourcesRef.current] + .filter((source) => !staticSourceValues.has(source) && !hiddenJobSources.includes(source)) + .sort() + .map((source) => ({ label: SOURCE_DISPLAY[source]?.label ?? source, value: source })); + const mergedSourceOptions = [...sourceFilterOptions, ...dynamicSourceOptions]; + const makeColumns: ComponentProps>['makeColumns'] = ({ accessor, }) => [ @@ -124,11 +137,12 @@ export const JobsDataView = () => { id: 'source', header: 'Source', size: 200, + enableSorting: true, meta: { filter: { type: 'single-select' as const, label: 'Source', - options: sourceFilterOptions, + options: mergedSourceOptions, }, }, cell: ({ row, column: col }) => {