From c470427d98ee3f58bd67ac62134e38a572d6391b Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Tue, 26 May 2026 10:25:51 -0700 Subject: [PATCH 1/2] fix(studio): Operators type for query filter Signed-off-by: Sean Teramae --- .../src/api/entity-store/useBaseModels.ts | 18 ++++--- .../common/src/api/filterOperators.ts | 48 +++++++++++++++++++ .../src/hooks/useStudioDataViewState/index.ts | 31 ++++++++---- web/packages/studio/src/api/intake/utils.ts | 13 ++--- .../DataDesignerJobsDataView/index.tsx | 7 ++- .../dataViews/DeploymentsDataView/index.tsx | 7 ++- .../EvaluationBenchmarksDataView/index.tsx | 15 ++++-- .../EvaluationMetricsDataView/index.tsx | 15 ++++-- .../dataViews/ExportJobsDataView/index.tsx | 6 +-- .../InferenceProvidersDataView/index.tsx | 7 ++- .../dataViews/JobsDataView/index.tsx | 10 +++- .../SafeSynthesizerJobsDataView/index.tsx | 7 ++- .../sidePanels/MetricRunSidePanel/index.tsx | 10 +++- .../FilesetSearchableSelect.tsx | 7 +-- .../routes/WorkspaceBaseModelsRoute/index.tsx | 8 ++-- .../agents/AgentSuggestionsRoute/index.tsx | 12 +++-- web/packages/studio/src/routes/utils.ts | 4 +- 17 files changed, 158 insertions(+), 67 deletions(-) create mode 100644 web/packages/common/src/api/filterOperators.ts diff --git a/web/packages/common/src/api/entity-store/useBaseModels.ts b/web/packages/common/src/api/entity-store/useBaseModels.ts index 2daba11f51d..2364dc40403 100644 --- a/web/packages/common/src/api/entity-store/useBaseModels.ts +++ b/web/packages/common/src/api/entity-store/useBaseModels.ts @@ -14,6 +14,7 @@ import { useCallback, useMemo } from 'react'; import { DEFAULT_NAMESPACE } from '../../constants'; import { DEFAULT_PAGE_SIZE, QUERY_PREFIX_ENTITY_STORE } from '../../constants/api'; import { isBaseModel } from '../../utils/models'; +import type { WithFilterOperators } from '../filterOperators'; const SORT_COMPARATORS: Record number> = { [ModelEntitySortField.name]: (a, b) => (a.name ?? '').localeCompare(b.name ?? ''), @@ -29,16 +30,13 @@ const SORT_COMPARATORS: Record & { - name?: string | { $like?: string; $eq?: string }; -}; +export type ModelEntityFilterInput = WithFilterOperators; export interface UseBaseModelsOptions { workspace: string; @@ -92,7 +90,7 @@ export function useBaseModels({ const isDefaultWorkspace = workspace === DEFAULT_NAMESPACE; const baseQuery = { - // Cast reflects the SDK's under-typing of `name`; see ModelEntityFilterInput doc above. + // Cast reflects the SDK's scalar-only modeling of filter fields; see ModelEntityFilterInput. filter: { base_model: false, ...filter } as ModelEntityFilter, page_size: DEFAULT_PAGE_SIZE, }; diff --git a/web/packages/common/src/api/filterOperators.ts b/web/packages/common/src/api/filterOperators.ts new file mode 100644 index 00000000000..bd637ef2439 --- /dev/null +++ b/web/packages/common/src/api/filterOperators.ts @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Mongo-style comparison operators accepted by NeMo Platform's unified filter + * syntax (e.g. `{ name: { $like: '%foo%' } }`, `{ created_at: { $gte, $lte } }`). + * + * The OpenAPI-generated SDK types model filter fields as bare scalars + * (`name?: string`) and do not expose the operator-object form. Use + * {@link WithFilterOperators} to widen a generated filter shape so call sites + * can build operator filters without `as unknown as` casts. + */ +export interface FilterOperators { + $eq?: V; + $ne?: V; + $in?: readonly V[]; + $nin?: readonly V[]; + $gt?: V; + $gte?: V; + $lt?: V; + $lte?: V; + /** Substring match (e.g. `%foo%`). String-typed regardless of `V`. */ + $like?: string; +} + +/** + * Widens each field of `F` so it accepts either its original scalar value or + * a {@link FilterOperators} object. Intended for API call sites that need + * operator-object filters; coerce back to the generated filter type once at + * the SDK boundary. + * + * @example + * type ModelFilterInput = WithFilterOperators; + * const filter: ModelFilterInput = { name: { $like: '%foo%' } }; + */ +export type WithFilterOperators = { + [K in keyof F]?: F[K] | FilterOperators>; +}; + +/** + * Build a typed operator-filter and coerce it to the generated SDK filter type + * in one step. Centralizes the unavoidable cast at the SDK boundary so call + * sites can be written with full type-checking on operator objects. + * + * @example + * filter: withOperators({ name: { $like: `%${search}%` } }) + */ +export const withOperators = (filter: WithFilterOperators): F => filter as F; diff --git a/web/packages/common/src/hooks/useStudioDataViewState/index.ts b/web/packages/common/src/hooks/useStudioDataViewState/index.ts index 7a65503f224..544a47cd9aa 100644 --- a/web/packages/common/src/hooks/useStudioDataViewState/index.ts +++ b/web/packages/common/src/hooks/useStudioDataViewState/index.ts @@ -39,15 +39,23 @@ export interface UseStudioDataViewStateOptions extends Omit< defaultSort?: { id: string; desc: boolean }; } -export interface ApiFilter { +/** + * API filter object exposed by the hook. The `filter` shape is parameterized by + * `FilterType` so consumers get type-checked filter keys and values without + * needing `as` casts at the call site. + * + * Defaults to `Record` for callers that don't supply a type. + */ +export interface ApiFilter> { searchText?: string; - filter?: Record; + filter?: Partial; } /** * Extended DataView state that includes helper functions for common table operations. */ -export interface StudioDataViewState extends DataView.DataViewState { +export interface StudioDataViewState> + extends DataView.DataViewState { /** * Resets pagination to page 1 and clears row selection. * Call this when search/filter criteria change to ensure users see results from the beginning. @@ -64,7 +72,7 @@ export interface StudioDataViewState extends DataView.DataViewState { * mapping `searchText` onto the appropriate filter field (and wrapping it in an operator * like `$like` if fuzzy matching is desired). */ - apiFilter: ApiFilter; + apiFilter: ApiFilter; /** Clears searchBar, columnFilters, and resets pagination. */ resetFilters: () => void; } @@ -91,9 +99,9 @@ export interface StudioDataViewState extends DataView.DataViewState { * - Use `resetPagination()` when search/filter criteria change. * - Intended for table/data grid views where browser-driven navigation is desirable. */ -export const useStudioDataViewState = ( +export const useStudioDataViewState = >( options?: UseStudioDataViewStateOptions -): StudioDataViewState => { +): StudioDataViewState => { const { defaultPage = DEFAULT_PAGE, defaultPageSize = DEFAULT_PAGE_SIZE, @@ -453,9 +461,12 @@ export const useStudioDataViewState = ( } }, [dataViewState.columnFiltering, urlColumnFilters, urlFiltersParam, updateUrlParams]); - // Convention-mapped API filter object - const apiFilter = useMemo(() => { - const result: ApiFilter = {}; + // Convention-mapped API filter object. + // The narrowing from TanStack's untyped `ColumnFiltersState` ({id: string, value: unknown}[]) + // to `Partial` happens here in one place, so call sites get a typed + // `apiFilter.filter` and don't need their own `as` casts. + const apiFilter = useMemo>(() => { + const result: ApiFilter = {}; if (debouncedSearchBar) { result.searchText = debouncedSearchBar; @@ -475,7 +486,7 @@ export const useStudioDataViewState = ( return true; }) .map((f) => [f.id, f.value]) - ); + ) as Partial; } return result; diff --git a/web/packages/studio/src/api/intake/utils.ts b/web/packages/studio/src/api/intake/utils.ts index 72ca7b9c070..0507a734518 100644 --- a/web/packages/studio/src/api/intake/utils.ts +++ b/web/packages/studio/src/api/intake/utils.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { EntryFilter } from '@nemo/sdk/generated/platform/schema'; import { QUERY_PARAMETERS } from '@studio/routes/constants'; import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; @@ -12,11 +13,7 @@ import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; * @param params - The URLSearchParams instance to populate * @param prefix - The current key prefix for nested properties (e.g., 'context', 'created_at') */ -const processFilterObject = ( - obj: Record, - params: URLSearchParams, - prefix = '' -): void => { +const processFilterObject = (obj: EntryFilter, params: URLSearchParams, prefix = ''): void => { for (const [key, value] of Object.entries(obj)) { // Skip undefined and null values if (value === undefined || value === null) { @@ -36,7 +33,7 @@ const processFilterObject = ( } // Handle nested objects recursively else if (typeof value === 'object' && value !== null) { - processFilterObject(value as Record, params, paramKey); + processFilterObject(value, params, paramKey); } // Handle primitive values (string, number, boolean) else { @@ -45,7 +42,7 @@ const processFilterObject = ( } }; -export const generateFilterParam = (filter?: Record): string => { +export const generateFilterParam = (filter?: EntryFilter): string => { if (!filter) { return ''; } @@ -57,7 +54,7 @@ export const generateFilterParam = (filter?: Record): string => const withoutProject = { ...filter, project: undefined }; processFilterObject(withoutProject, params); } else { - processFilterObject(filter as Record, params); + processFilterObject(filter, params); } return params.toString(); diff --git a/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx index 5bb9983fa29..89053d91928 100644 --- a/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/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 { withOperators } from '@nemo/common/src/api/filterOperators'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { ROW_SELECTION_COLUMN_SIZE, @@ -86,9 +87,11 @@ export const DataDesignerJobsDataView: FC = () => { filter: { ...((dataViewState.apiFilter.filter ?? {}) as DataDesignerJobsListFilter), ...(dataViewState.apiFilter.searchText - ? { name: { $like: dataViewState.apiFilter.searchText } } + ? withOperators({ + name: { $like: dataViewState.apiFilter.searchText }, + }) : {}), - } as DataDesignerJobsListFilter | undefined, + }, }, { query: { diff --git a/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx b/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx index 9a226b65c93..7b05dc2ff24 100644 --- a/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx @@ -10,6 +10,7 @@ * its affiliates is strictly prohibited. */ +import { withOperators } from '@nemo/common/src/api/filterOperators'; 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'; @@ -68,9 +69,11 @@ export const DeploymentsDataView: FC = ({ filter: { ...dataViewState.apiFilter.filter, ...(dataViewState.apiFilter.searchText - ? { name: { $like: dataViewState.apiFilter.searchText } as unknown as string } + ? withOperators({ + name: { $like: dataViewState.apiFilter.searchText }, + }) : {}), - } as ModelDeploymentFilter, + }, }, { query: { diff --git a/web/packages/studio/src/components/dataViews/EvaluationBenchmarksDataView/index.tsx b/web/packages/studio/src/components/dataViews/EvaluationBenchmarksDataView/index.tsx index 939cec8a310..7517991cc34 100644 --- a/web/packages/studio/src/components/dataViews/EvaluationBenchmarksDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/EvaluationBenchmarksDataView/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 { withOperators, type WithFilterOperators } from '@nemo/common/src/api/filterOperators'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; @@ -9,7 +10,10 @@ import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParam } from '@nemo/common/src/utils/query'; import { useEvaluationListBenchmarks } from '@nemo/sdk/generated/platform/api'; -import type { EvaluationListBenchmarksSort } from '@nemo/sdk/generated/platform/schema'; +import type { + EvaluationListBenchmarksParams, + EvaluationListBenchmarksSort, +} from '@nemo/sdk/generated/platform/schema'; import { Button, Stack, Text } from '@nvidia/foundations-react-core'; import type { BenchmarkItemWithId, @@ -31,11 +35,12 @@ export const EvaluationBenchmarksDataView: FC }); const benchmarksFilter = useMemo(() => { - const filterObj: Record = {}; + type BenchmarksFilter = NonNullable; + const filterObj: WithFilterOperators = {}; const nameFilter = dataViewState.apiFilter.searchText; if (nameFilter) { - filterObj.name = { $like: nameFilter } as unknown as string; + filterObj.name = { $like: nameFilter }; } const createdAtFilter = dataViewState.apiFilter.filter?.created_at; @@ -44,11 +49,11 @@ export const EvaluationBenchmarksDataView: FC typeof createdAtFilter === 'object' && Object.keys(createdAtFilter).length > 0 ) { - filterObj.created_at = createdAtFilter; + filterObj.created_at = createdAtFilter as BenchmarksFilter['created_at']; } if (Object.keys(filterObj).length === 0) return undefined; - return filterObj; + return withOperators(filterObj); }, [dataViewState.apiFilter]); const { diff --git a/web/packages/studio/src/components/dataViews/EvaluationMetricsDataView/index.tsx b/web/packages/studio/src/components/dataViews/EvaluationMetricsDataView/index.tsx index 1f0ea2dbd60..61716d05f53 100644 --- a/web/packages/studio/src/components/dataViews/EvaluationMetricsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/EvaluationMetricsDataView/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 { withOperators, type WithFilterOperators } from '@nemo/common/src/api/filterOperators'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { ROW_ACTIONS_COLUMN_SIZE, @@ -17,7 +18,10 @@ import { useEvaluationDeleteMetric, useEvaluationListMetrics, } from '@nemo/sdk/generated/platform/api'; -import type { EvaluationListMetricsSort } from '@nemo/sdk/generated/platform/schema'; +import type { + EvaluationListMetricsParams, + EvaluationListMetricsSort, +} from '@nemo/sdk/generated/platform/schema'; import { Badge, Button, Flex, Stack, Text } from '@nvidia/foundations-react-core'; import type { EvaluationMetricsDataViewProps, @@ -73,11 +77,12 @@ export const EvaluationMetricsDataView: FC = ({ // Build filter object from the dataview filter state const metricsFilter = useMemo(() => { - const filterObj: Record = {}; + type MetricsFilter = NonNullable; + const filterObj: WithFilterOperators = {}; const nameFilter = dataViewState.apiFilter.searchText; if (nameFilter) { - filterObj.name = { $like: nameFilter } as unknown as string; + filterObj.name = { $like: nameFilter }; } const createdAtFilter = dataViewState.apiFilter.filter?.created_at; @@ -86,11 +91,11 @@ export const EvaluationMetricsDataView: FC = ({ typeof createdAtFilter === 'object' && Object.keys(createdAtFilter).length > 0 ) { - filterObj.created_at = createdAtFilter; + filterObj.created_at = createdAtFilter as MetricsFilter['created_at']; } if (Object.keys(filterObj).length === 0) return undefined; - return filterObj; + return withOperators(filterObj); }, [dataViewState.apiFilter]); const { diff --git a/web/packages/studio/src/components/dataViews/ExportJobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExportJobsDataView/index.tsx index df125bd946e..8312f8e4966 100644 --- a/web/packages/studio/src/components/dataViews/ExportJobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExportJobsDataView/index.tsx @@ -31,7 +31,7 @@ export const ExportJobsDataView = () => { const { getQueryParam, setQueryParam } = useQueryParams(); const exportJobId = getQueryParam(QUERY_PARAMETERS.exportJobId); - const dataViewState = useStudioDataViewState({ + const dataViewState = useStudioDataViewState({ defaultSort: { id: 'created_at', desc: true }, }); @@ -49,9 +49,9 @@ export const ExportJobsDataView = () => { page_size: dataViewState.pagination.state.pageSize, sort: getSortParam(dataViewState.sorting.state) as ExportJobSortField, filter: { - ...(dataViewState.apiFilter.filter as ExportJobFilter | undefined), + ...dataViewState.apiFilter.filter, ...(dataViewState.apiFilter.searchText ? { id: dataViewState.apiFilter.searchText } : {}), - } as ExportJobFilter | undefined, + }, }, { query: { diff --git a/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx b/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx index 4a1f46f2198..25b5ef3d4e0 100644 --- a/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx @@ -10,6 +10,7 @@ * its affiliates is strictly prohibited. */ +import { withOperators } from '@nemo/common/src/api/filterOperators'; import { ROW_ACTIONS_COLUMN_SIZE, StudioDataView, @@ -89,9 +90,11 @@ export const InferenceProvidersDataView: FC = ( filter: { ...dataViewState.apiFilter.filter, ...(dataViewState.apiFilter.searchText - ? { name: { $like: dataViewState.apiFilter.searchText } as unknown as string } + ? withOperators({ + name: { $like: dataViewState.apiFilter.searchText }, + }) : {}), - } as ModelProviderFilter, + }, }, { query: { diff --git a/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx index ff4ca41bec1..998b3e47287 100644 --- a/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/JobsDataView/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 { withOperators } from '@nemo/common/src/api/filterOperators'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; @@ -12,6 +13,7 @@ import { useJobsListJobs } from '@nemo/sdk/generated/platform/api'; import type { PlatformJobResponse, PlatformJobSortField, + PlatformJobsListFilter, } from '@nemo/sdk/generated/platform/schema'; import { Button, Flex, StatusMessage } from '@nvidia/foundations-react-core'; import { getErrorMessage } from '@studio/api/common/utils'; @@ -126,9 +128,13 @@ export const JobsDataView = () => { sort: getSortParam(dataViewState.sorting.state) as PlatformJobSortField, filter: { ...userFilter, - ...(hasUserSourceFilter ? {} : { source: { $nin: hiddenJobSources } as unknown as string }), + ...(hasUserSourceFilter + ? {} + : withOperators({ source: { $nin: hiddenJobSources } })), ...(dataViewState.apiFilter.searchText - ? { name: { $like: dataViewState.apiFilter.searchText } as unknown as string } + ? withOperators({ + name: { $like: dataViewState.apiFilter.searchText }, + }) : {}), }, }, diff --git a/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx index 6b0fcb27049..95658447f58 100644 --- a/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/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 { withOperators } from '@nemo/common/src/api/filterOperators'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import * as DataView from '@nemo/common/src/components/DataView/internal'; import { @@ -98,9 +99,11 @@ export const SafeSynthesizerJobsDataView: FC = () => { filter: { ...((dataViewState.apiFilter.filter ?? {}) as SafeSynthesizerJobsListFilter), ...(dataViewState.apiFilter.searchText - ? { name: { $like: dataViewState.apiFilter.searchText } } + ? withOperators({ + name: { $like: dataViewState.apiFilter.searchText }, + }) : {}), - } as SafeSynthesizerJobsListFilter | undefined, + }, }, { query: { diff --git a/web/packages/studio/src/components/sidePanels/MetricRunSidePanel/index.tsx b/web/packages/studio/src/components/sidePanels/MetricRunSidePanel/index.tsx index 39ac43d5da6..6fb1140865f 100644 --- a/web/packages/studio/src/components/sidePanels/MetricRunSidePanel/index.tsx +++ b/web/packages/studio/src/components/sidePanels/MetricRunSidePanel/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 { withOperators } from '@nemo/common/src/api/filterOperators'; import { BASIC_ALL_MODELS_DROPDOWN_FILTER } from '@nemo/common/src/api/models/useModels'; import { useModelsFromWorkspace } from '@nemo/common/src/api/models/useModelsFromWorkspace'; import { VariableButton } from '@nemo/common/src/components/buttons/VariableButton'; @@ -17,7 +18,10 @@ import { useEvaluationCreateMetricJob, useEvaluationListMetrics, } from '@nemo/sdk/generated/platform/api'; -import type { EvaluatorModel } from '@nemo/sdk/generated/platform/schema'; +import type { + EvaluationListMetricsParams, + EvaluatorModel, +} from '@nemo/sdk/generated/platform/schema'; import type { MetricEvaluationJobRequest } from '@nemo/sdk/generated/platform/schema/MetricEvaluationJobRequest'; import { Button, @@ -186,7 +190,9 @@ export const MetricRunSidePanel: FC = ({ page: 1, page_size: 50, filter: metricSearch - ? ({ name: { $like: metricSearch } } as Record) + ? withOperators>({ + name: { $like: metricSearch }, + }) : undefined, }, { query: { enabled: !metric && open } } diff --git a/web/packages/studio/src/routes/DeploymentsListRoute/CreateDeploymentSidePanel/FilesetSearchableSelect.tsx b/web/packages/studio/src/routes/DeploymentsListRoute/CreateDeploymentSidePanel/FilesetSearchableSelect.tsx index c11ef322b58..1aa885af71a 100644 --- a/web/packages/studio/src/routes/DeploymentsListRoute/CreateDeploymentSidePanel/FilesetSearchableSelect.tsx +++ b/web/packages/studio/src/routes/DeploymentsListRoute/CreateDeploymentSidePanel/FilesetSearchableSelect.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { withOperators } from '@nemo/common/src/api/filterOperators'; import { ControlledSearchableSelect, type SelectItemOption, @@ -39,9 +40,9 @@ export function FilesetSearchableSelect({ const [search, setSearch] = useState(''); const filter = useMemo(() => { if (!search) return undefined; - // `$like` enables substring search; the generated `name?: string` type doesn't expose - // the operator object form so we cast — same pattern as CustomModelsDataView. - return { name: { $like: `%${search}%` } } as unknown as FilesListFilesetsParams['filter']; + return withOperators({ + name: { $like: `%${search}%` }, + }); }, [search]); const { diff --git a/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx b/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx index 8d72bc17ecf..be7f60519a8 100644 --- a/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx +++ b/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx @@ -120,7 +120,9 @@ export const WorkspaceBaseModelsRoute: FC = () => { items: [{ slotLabel: 'Base Models' }], }); - const dataViewState = useStudioDataViewState({ + const dataViewState = useStudioDataViewState< + Partial & { [CUSTOMIZABLE_FILTER_ID]?: CustomizableFilterState } + >({ defaultSort: { id: 'name', desc: false }, }); @@ -130,9 +132,7 @@ export const WorkspaceBaseModelsRoute: FC = () => { const modelNameFromPath = decodeURIComponent(modelNameParam ?? ''); const nameSearch = dataViewState.apiFilter.searchText; - const allColumnFilters = dataViewState.apiFilter.filter as - | (Partial & { [CUSTOMIZABLE_FILTER_ID]?: CustomizableFilterState }) - | undefined; + const allColumnFilters = dataViewState.apiFilter.filter; const customizableFilter = CUSTOMIZER_ENABLED ? allColumnFilters?.[CUSTOMIZABLE_FILTER_ID] : undefined; diff --git a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx index 4202ddbbda4..29456ee73b9 100644 --- a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx +++ b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx @@ -70,6 +70,12 @@ import { import { useLocation } from 'react-router-dom'; type MultiState = Record; +type SuggestionFilter = { + agent?: MultiState; + severity?: MultiState; + type?: MultiState; + scope?: MultiState; +}; export const AgentOptimizationsRoute: FC = () => { const workspace = useWorkspaceFromPath(); @@ -161,7 +167,7 @@ export const AgentOptimizationsRoute: FC = () => { // Suggestion the user just clicked Apply on — drives the eval-config // chooser modal. ``null`` keeps the modal closed. const [pendingApply, setPendingApply] = useState(null); - const dataViewState = useStudioDataViewState({}); + const dataViewState = useStudioDataViewState({}); const handleApplyClicked = useCallback( (suggestion: OptimizationSuggestion) => { @@ -272,9 +278,7 @@ export const AgentOptimizationsRoute: FC = () => { [typeOptions, severityOptions, scopeOptions, agentOptions] ); - const filterState = dataViewState.apiFilter.filter as - | { agent?: MultiState; severity?: MultiState; type?: MultiState; scope?: MultiState } - | undefined; + const filterState = dataViewState.apiFilter.filter; const agentFilter = filterState?.agent; const severityFilter = filterState?.severity; const typeFilter = filterState?.type; diff --git a/web/packages/studio/src/routes/utils.ts b/web/packages/studio/src/routes/utils.ts index 7ccd819c285..a36c22040f6 100644 --- a/web/packages/studio/src/routes/utils.ts +++ b/web/packages/studio/src/routes/utils.ts @@ -389,9 +389,7 @@ export const getIntakeRoute = (workspace: string) => { }; export const getIntakeEntriesRoute = (workspace: string, options?: ListEntriesParams) => { - const queryParamStr = options - ? `?${generateFilterParam(options?.filter as Record)}` - : ''; + const queryParamStr = options ? `?${generateFilterParam(options.filter)}` : ''; return generatePath(ROUTES.workspace.intakeEntries, { workspace }) + queryParamStr; }; From 923f698bb0785b66378cb54ab7c9e0f3e2cf62cf Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Tue, 26 May 2026 12:01:38 -0700 Subject: [PATCH 2/2] fix PR comments Signed-off-by: Sean Teramae --- web/packages/common/src/api/entity-store/useBaseModels.ts | 2 +- web/packages/studio/src/api/intake/utils.ts | 2 +- .../studio/src/routes/agents/AgentSuggestionsRoute/index.tsx | 4 ++-- web/packages/studio/src/routes/utils.ts | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/packages/common/src/api/entity-store/useBaseModels.ts b/web/packages/common/src/api/entity-store/useBaseModels.ts index 2364dc40403..245991626d8 100644 --- a/web/packages/common/src/api/entity-store/useBaseModels.ts +++ b/web/packages/common/src/api/entity-store/useBaseModels.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import type { WithFilterOperators } from '@nemo/common/src/api/filterOperators'; import { modelsListModels } from '@nemo/sdk/generated/platform/api'; import { ModelEntity, @@ -14,7 +15,6 @@ import { useCallback, useMemo } from 'react'; import { DEFAULT_NAMESPACE } from '../../constants'; import { DEFAULT_PAGE_SIZE, QUERY_PREFIX_ENTITY_STORE } from '../../constants/api'; import { isBaseModel } from '../../utils/models'; -import type { WithFilterOperators } from '../filterOperators'; const SORT_COMPARATORS: Record number> = { [ModelEntitySortField.name]: (a, b) => (a.name ?? '').localeCompare(b.name ?? ''), diff --git a/web/packages/studio/src/api/intake/utils.ts b/web/packages/studio/src/api/intake/utils.ts index 0507a734518..31eecfdd29c 100644 --- a/web/packages/studio/src/api/intake/utils.ts +++ b/web/packages/studio/src/api/intake/utils.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { EntryFilter } from '@nemo/sdk/generated/platform/schema'; +import type { EntryFilter } from '@nemo/sdk/generated/platform/schema'; import { QUERY_PARAMETERS } from '@studio/routes/constants'; import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; diff --git a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx index 29456ee73b9..621ac11a932 100644 --- a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx +++ b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/index.tsx @@ -70,12 +70,12 @@ import { import { useLocation } from 'react-router-dom'; type MultiState = Record; -type SuggestionFilter = { +interface SuggestionFilter { agent?: MultiState; severity?: MultiState; type?: MultiState; scope?: MultiState; -}; +} export const AgentOptimizationsRoute: FC = () => { const workspace = useWorkspaceFromPath(); diff --git a/web/packages/studio/src/routes/utils.ts b/web/packages/studio/src/routes/utils.ts index a36c22040f6..35199d6101d 100644 --- a/web/packages/studio/src/routes/utils.ts +++ b/web/packages/studio/src/routes/utils.ts @@ -389,7 +389,8 @@ export const getIntakeRoute = (workspace: string) => { }; export const getIntakeEntriesRoute = (workspace: string, options?: ListEntriesParams) => { - const queryParamStr = options ? `?${generateFilterParam(options.filter)}` : ''; + const filterQuery = options ? generateFilterParam(options.filter) : ''; + const queryParamStr = filterQuery ? `?${filterQuery}` : ''; return generatePath(ROUTES.workspace.intakeEntries, { workspace }) + queryParamStr; };