Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions web/packages/common/src/api/entity-store/useBaseModels.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -29,16 +30,13 @@ const SORT_COMPARATORS: Record<ModelEntitySortField, (a: ModelEntity, b: ModelEn
};

/**
* Widened input type for `useBaseModels({ filter })`. The generated SDK types
* `ModelEntityFilter.name` as `string` only, but the NeMo Platform API documents support
* for filter-operator objects (`{ $like, $eq }`) on `name` via the same unified
* filter syntax used for `created_at` / `updated_at`. We widen here so call sites
* can build text-search filters without bypassing the type system; the coercion
* back to `ModelEntityFilter` happens once at the SDK boundary below.
* Widened input type for `useBaseModels({ filter })`. The generated SDK models
* filter fields as bare scalars, but the NeMo Platform API accepts operator
* objects on every field via the same unified filter syntax (`$like`, `$gte`,
* etc). Coercion back to `ModelEntityFilter` happens once at the SDK boundary
* below.
*/
export type ModelEntityFilterInput = Omit<ModelEntityFilter, 'name'> & {
name?: string | { $like?: string; $eq?: string };
};
export type ModelEntityFilterInput = WithFilterOperators<ModelEntityFilter>;

export interface UseBaseModelsOptions {
workspace: string;
Expand Down Expand Up @@ -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,
};
Expand Down
48 changes: 48 additions & 0 deletions web/packages/common/src/api/filterOperators.ts
Original file line number Diff line number Diff line change
@@ -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<V> {
Comment thread
steramae-nvidia marked this conversation as resolved.
Resolved
$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<ModelEntityFilter>;
* const filter: ModelFilterInput = { name: { $like: '%foo%' } };
*/
export type WithFilterOperators<F> = {
[K in keyof F]?: F[K] | FilterOperators<NonNullable<F[K]>>;
};

/**
* 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<FilesetFilter>({ name: { $like: `%${search}%` } })
*/
export const withOperators = <F>(filter: WithFilterOperators<F>): F => filter as F;
31 changes: 21 additions & 10 deletions web/packages/common/src/hooks/useStudioDataViewState/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>` for callers that don't supply a type.
*/
export interface ApiFilter<FilterType = Record<string, unknown>> {
searchText?: string;
filter?: Record<string, unknown>;
filter?: Partial<FilterType>;
}

/**
* Extended DataView state that includes helper functions for common table operations.
*/
export interface StudioDataViewState extends DataView.DataViewState {
export interface StudioDataViewState<FilterType = Record<string, unknown>>
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.
Expand All @@ -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<FilterType>;
/** Clears searchBar, columnFilters, and resets pagination. */
resetFilters: () => void;
}
Expand All @@ -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 = <FilterType = Record<string, unknown>>(
options?: UseStudioDataViewStateOptions
): StudioDataViewState => {
): StudioDataViewState<FilterType> => {
const {
defaultPage = DEFAULT_PAGE,
defaultPageSize = DEFAULT_PAGE_SIZE,
Expand Down Expand Up @@ -453,9 +461,12 @@ export const useStudioDataViewState = (
}
}, [dataViewState.columnFiltering, urlColumnFilters, urlFiltersParam, updateUrlParams]);

// Convention-mapped API filter object
const apiFilter = useMemo<ApiFilter>(() => {
const result: ApiFilter = {};
// Convention-mapped API filter object.
// The narrowing from TanStack's untyped `ColumnFiltersState` ({id: string, value: unknown}[])
// to `Partial<FilterType>` happens here in one place, so call sites get a typed
// `apiFilter.filter` and don't need their own `as` casts.
const apiFilter = useMemo<ApiFilter<FilterType>>(() => {
const result: ApiFilter<FilterType> = {};

if (debouncedSearchBar) {
result.searchText = debouncedSearchBar;
Expand All @@ -475,7 +486,7 @@ export const useStudioDataViewState = (
return true;
})
.map((f) => [f.id, f.value])
);
) as Partial<FilterType>;
}

return result;
Expand Down
13 changes: 5 additions & 8 deletions web/packages/studio/src/api/intake/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { EntryFilter } from '@nemo/sdk/generated/platform/schema';
import { QUERY_PARAMETERS } from '@studio/routes/constants';
import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';

Expand All @@ -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<string, unknown>,
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) {
Expand All @@ -36,7 +33,7 @@ const processFilterObject = (
}
// Handle nested objects recursively
else if (typeof value === 'object' && value !== null) {
processFilterObject(value as Record<string, unknown>, params, paramKey);
processFilterObject(value, params, paramKey);
}
// Handle primitive values (string, number, boolean)
else {
Expand All @@ -45,7 +42,7 @@ const processFilterObject = (
}
};

export const generateFilterParam = (filter?: Record<string, unknown>): string => {
export const generateFilterParam = (filter?: EntryFilter): string => {
if (!filter) {
return '';
}
Expand All @@ -57,7 +54,7 @@ export const generateFilterParam = (filter?: Record<string, unknown>): string =>
const withoutProject = { ...filter, project: undefined };
processFilterObject(withoutProject, params);
} else {
processFilterObject(filter as Record<string, unknown>, params);
processFilterObject(filter, params);
}

return params.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -86,9 +87,11 @@ export const DataDesignerJobsDataView: FC = () => {
filter: {
...((dataViewState.apiFilter.filter ?? {}) as DataDesignerJobsListFilter),
...(dataViewState.apiFilter.searchText
? { name: { $like: dataViewState.apiFilter.searchText } }
? withOperators<DataDesignerJobsListFilter>({
name: { $like: dataViewState.apiFilter.searchText },
})
: {}),
} as DataDesignerJobsListFilter | undefined,
},
},
{
query: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,9 +69,11 @@ export const DeploymentsDataView: FC<DeploymentsDataViewProps> = ({
filter: {
...dataViewState.apiFilter.filter,
...(dataViewState.apiFilter.searchText
? { name: { $like: dataViewState.apiFilter.searchText } as unknown as string }
? withOperators<ModelDeploymentFilter>({
name: { $like: dataViewState.apiFilter.searchText },
})
: {}),
} as ModelDeploymentFilter,
},
},
{
query: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -31,11 +35,12 @@ export const EvaluationBenchmarksDataView: FC<EvaluationBenchmarksDataViewProps>
});

const benchmarksFilter = useMemo(() => {
const filterObj: Record<string, unknown> = {};
type BenchmarksFilter = NonNullable<EvaluationListBenchmarksParams['filter']>;
const filterObj: WithFilterOperators<BenchmarksFilter> = {};

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;
Expand All @@ -44,11 +49,11 @@ export const EvaluationBenchmarksDataView: FC<EvaluationBenchmarksDataViewProps>
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<BenchmarksFilter>(filterObj);
}, [dataViewState.apiFilter]);

const {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -73,11 +77,12 @@ export const EvaluationMetricsDataView: FC<EvaluationMetricsDataViewProps> = ({

// Build filter object from the dataview filter state
const metricsFilter = useMemo(() => {
const filterObj: Record<string, unknown> = {};
type MetricsFilter = NonNullable<EvaluationListMetricsParams['filter']>;
const filterObj: WithFilterOperators<MetricsFilter> = {};

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;
Expand All @@ -86,11 +91,11 @@ export const EvaluationMetricsDataView: FC<EvaluationMetricsDataViewProps> = ({
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<MetricsFilter>(filterObj);
}, [dataViewState.apiFilter]);

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const ExportJobsDataView = () => {
const { getQueryParam, setQueryParam } = useQueryParams();
const exportJobId = getQueryParam(QUERY_PARAMETERS.exportJobId);

const dataViewState = useStudioDataViewState({
const dataViewState = useStudioDataViewState<ExportJobFilter>({
defaultSort: { id: 'created_at', desc: true },
});

Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* its affiliates is strictly prohibited.
*/

import { withOperators } from '@nemo/common/src/api/filterOperators';
import {
ROW_ACTIONS_COLUMN_SIZE,
StudioDataView,
Expand Down Expand Up @@ -89,9 +90,11 @@ export const InferenceProvidersDataView: FC<InferenceProvidersDataViewProps> = (
filter: {
...dataViewState.apiFilter.filter,
...(dataViewState.apiFilter.searchText
? { name: { $like: dataViewState.apiFilter.searchText } as unknown as string }
? withOperators<ModelProviderFilter>({
name: { $like: dataViewState.apiFilter.searchText },
})
: {}),
} as ModelProviderFilter,
},
},
{
query: {
Expand Down
Loading
Loading