Skip to content

Commit 243b46d

Browse files
Merge pull request #1670 from kubeflow/main
[pull] main from kubeflow:main
2 parents 9615ac2 + 3b31a7d commit 243b46d

25 files changed

Lines changed: 193 additions & 1648 deletions

catalog/internal/catalog/mcpcatalog/service/mcp_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ func (r *MCPServerRepositoryImpl) applyCustomOrdering(query *gorm.DB, listOption
472472

473473
// Handle NAME ordering
474474
if orderBy == "NAME" {
475-
return pagination.ApplyNameOrdering(query, contextTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize())
475+
return pagination.ApplyNameOrdering(query, contextTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize(), false)
476476
}
477477

478478
// Fall back to standard pagination

catalog/internal/catalog/modelcatalog/service/catalog_model.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ func (r *CatalogModelRepositoryImpl) applyCustomOrdering(query *gorm.DB, listOpt
470470

471471
// Handle NAME ordering specially (catalog-specific)
472472
if orderBy == "NAME" {
473-
return catpagination.ApplyNameOrdering(query, contextTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize())
473+
return catpagination.ApplyNameOrdering(query, contextTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize(), true)
474474
}
475475

476476
subquery, sortColumn := r.sortValueQuery(listOptions, contextTable+".id")
@@ -536,7 +536,11 @@ func (r *CatalogModelRepositoryImpl) applyCursorPagination(query *gorm.DB, curso
536536
func (r *CatalogModelRepositoryImpl) createPaginationToken(lastItem schema.Context, listOptions *models.CatalogModelListOptions) string {
537537
// Handle NAME ordering (catalog-specific)
538538
if listOptions.GetOrderBy() == "NAME" {
539-
return catpagination.CreateNamePaginationToken(lastItem.ID, &lastItem.Name)
539+
displayName := lastItem.Name
540+
if _, after, ok := strings.Cut(lastItem.Name, ":"); ok {
541+
displayName = after
542+
}
543+
return catpagination.CreateNamePaginationToken(lastItem.ID, &displayName)
540544
}
541545

542546
sortValueQuery, column := r.sortValueQuery(listOptions)

catalog/internal/catalog/modelcatalog/service/catalog_model_test.go

Lines changed: 134 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -830,27 +830,38 @@ func TestCatalogModelRepository(t *testing.T) {
830830
})
831831

832832
t.Run("TestNameOrdering", func(t *testing.T) {
833-
// Create test models with specific names for ordering
834-
testModels := []string{
835-
"zebra-model",
836-
"alpha-model",
837-
"beta-model",
838-
"gamma-model",
839-
"delta-model",
840-
}
841-
842-
var savedModels []models.CatalogModel
843-
for _, name := range testModels {
833+
// Create models from two different sources so stored names have source prefixes.
834+
// This validates that sorting is by display name (after the colon) not the raw stored name.
835+
// Without the fix, "source-z:alpha-model" would sort after "source-a:zebra-model".
836+
type modelSpec struct {
837+
displayName string
838+
sourceID string
839+
}
840+
testModels := []modelSpec{
841+
// source-z prefix ensures raw-name sort would put alpha/beta last
842+
{displayName: "alpha-model", sourceID: "source-z"},
843+
{displayName: "beta-model", sourceID: "source-z"},
844+
// source-a prefix ensures raw-name sort would put gamma/zebra first
845+
{displayName: "gamma-model", sourceID: "source-a"},
846+
{displayName: "zebra-model", sourceID: "source-a"},
847+
{displayName: "delta-model", sourceID: "source-m"},
848+
// colon in display name: stored as "source-a:epsilon:v2", display name is "epsilon:v2"
849+
{displayName: "epsilon:v2", sourceID: "source-a"},
850+
}
851+
852+
for _, spec := range testModels {
844853
catalogModel := &models.CatalogModelImpl{
845854
Attributes: &models.CatalogModelAttributes{
846-
Name: apiutils.Of(name),
847-
ExternalID: apiutils.Of(name + "-ext"),
855+
Name: apiutils.Of(spec.displayName),
856+
ExternalID: apiutils.Of(spec.displayName + "-ord-ext"),
857+
},
858+
Properties: &[]dbmodels.Properties{
859+
{Name: "source_id", StringValue: apiutils.Of(spec.sourceID)},
848860
},
849861
}
850862

851-
savedModel, err := repo.Save(catalogModel)
863+
_, err := repo.Save(catalogModel)
852864
require.NoError(t, err)
853-
savedModels = append(savedModels, savedModel)
854865
}
855866

856867
// Test NAME ordering ASC
@@ -864,35 +875,48 @@ func TestCatalogModelRepository(t *testing.T) {
864875
require.NoError(t, err)
865876
require.NotNil(t, result)
866877

867-
// Extract our test model names from results
878+
// Extract our test model display names from results (repo returns stored namespaced names)
879+
displayNames := map[string]string{
880+
"source-z:alpha-model": "alpha-model",
881+
"source-z:beta-model": "beta-model",
882+
"source-a:gamma-model": "gamma-model",
883+
"source-a:zebra-model": "zebra-model",
884+
"source-m:delta-model": "delta-model",
885+
// display name contains a colon: stored name is "source-a:epsilon:v2"
886+
"source-a:epsilon:v2": "epsilon:v2",
887+
}
868888
var foundNames []string
869889
for _, model := range result.Items {
870-
name := *model.GetAttributes().Name
871-
if name == "zebra-model" || name == "alpha-model" || name == "beta-model" ||
872-
name == "gamma-model" || name == "delta-model" {
873-
foundNames = append(foundNames, name)
890+
storedName := *model.GetAttributes().Name
891+
if displayName, ok := displayNames[storedName]; ok {
892+
foundNames = append(foundNames, displayName)
874893
}
875894
}
876895

877896
// Verify we found all our test models
878-
require.GreaterOrEqual(t, len(foundNames), 5, "Should find all test models")
897+
require.GreaterOrEqual(t, len(foundNames), 6, "Should find all test models")
879898

880-
// Verify ASC ordering: alpha < beta < delta < gamma < zebra
899+
// Verify ASC ordering by display name: alpha < beta < delta < epsilon:v2 < gamma < zebra
900+
// Without the fix, source-z models (alpha, beta) would appear after source-a models (gamma, zebra).
901+
// Also verifies multi-colon display names ("epsilon:v2") sort correctly.
881902
alphaIdx := findIndex(foundNames, "alpha-model")
882903
betaIdx := findIndex(foundNames, "beta-model")
883904
deltaIdx := findIndex(foundNames, "delta-model")
905+
epsilonIdx := findIndex(foundNames, "epsilon:v2")
884906
gammaIdx := findIndex(foundNames, "gamma-model")
885907
zebraIdx := findIndex(foundNames, "zebra-model")
886908

887909
require.NotEqual(t, -1, alphaIdx, "alpha-model not found")
888910
require.NotEqual(t, -1, betaIdx, "beta-model not found")
889911
require.NotEqual(t, -1, deltaIdx, "delta-model not found")
912+
require.NotEqual(t, -1, epsilonIdx, "epsilon:v2 not found")
890913
require.NotEqual(t, -1, gammaIdx, "gamma-model not found")
891914
require.NotEqual(t, -1, zebraIdx, "zebra-model not found")
892915

893916
assert.Less(t, alphaIdx, betaIdx, "alpha should come before beta in ASC")
894917
assert.Less(t, betaIdx, deltaIdx, "beta should come before delta in ASC")
895-
assert.Less(t, deltaIdx, gammaIdx, "delta should come before gamma in ASC")
918+
assert.Less(t, deltaIdx, epsilonIdx, "delta should come before epsilon:v2 in ASC")
919+
assert.Less(t, epsilonIdx, gammaIdx, "epsilon:v2 should come before gamma in ASC")
896920
assert.Less(t, gammaIdx, zebraIdx, "gamma should come before zebra in ASC")
897921

898922
// Test NAME ordering DESC
@@ -909,22 +933,23 @@ func TestCatalogModelRepository(t *testing.T) {
909933
// Extract our test model names from DESC results
910934
foundNames = []string{}
911935
for _, model := range result.Items {
912-
name := *model.GetAttributes().Name
913-
if name == "zebra-model" || name == "alpha-model" || name == "beta-model" ||
914-
name == "gamma-model" || name == "delta-model" {
915-
foundNames = append(foundNames, name)
936+
storedName := *model.GetAttributes().Name
937+
if displayName, ok := displayNames[storedName]; ok {
938+
foundNames = append(foundNames, displayName)
916939
}
917940
}
918941

919-
// Verify DESC ordering: zebra > gamma > delta > beta > alpha
942+
// Verify DESC ordering: zebra > gamma > epsilon:v2 > delta > beta > alpha
920943
alphaIdxDesc := findIndex(foundNames, "alpha-model")
921944
betaIdxDesc := findIndex(foundNames, "beta-model")
922945
deltaIdxDesc := findIndex(foundNames, "delta-model")
946+
epsilonIdxDesc := findIndex(foundNames, "epsilon:v2")
923947
gammaIdxDesc := findIndex(foundNames, "gamma-model")
924948
zebraIdxDesc := findIndex(foundNames, "zebra-model")
925949

926950
assert.Less(t, zebraIdxDesc, gammaIdxDesc, "zebra should come before gamma in DESC")
927-
assert.Less(t, gammaIdxDesc, deltaIdxDesc, "gamma should come before delta in DESC")
951+
assert.Less(t, gammaIdxDesc, epsilonIdxDesc, "gamma should come before epsilon:v2 in DESC")
952+
assert.Less(t, epsilonIdxDesc, deltaIdxDesc, "epsilon:v2 should come before delta in DESC")
928953
assert.Less(t, deltaIdxDesc, betaIdxDesc, "delta should come before beta in DESC")
929954
assert.Less(t, betaIdxDesc, alphaIdxDesc, "beta should come before alpha in DESC")
930955
})
@@ -1023,6 +1048,86 @@ func TestCatalogModelRepository(t *testing.T) {
10231048
}
10241049
})
10251050

1051+
t.Run("TestNameOrderingMultiColonPagination", func(t *testing.T) {
1052+
// Regression test: display names containing a colon (e.g. "llama:7b" stored as "source:llama:7b")
1053+
// caused cursor-based pagination to break because SUBSTRING(name FROM STRPOS(name, ':') + 1)
1054+
// returns everything after the first colon (e.g. "llama:7b"), while strings.Cut does the same,
1055+
// keeping cursor values consistent with ORDER BY values.
1056+
multiColonModels := []struct {
1057+
displayName string
1058+
sourceID string
1059+
}{
1060+
{"mc-model:aa", "source-x"},
1061+
{"mc-model:bb", "source-x"},
1062+
{"mc-model:cc", "source-x"},
1063+
{"mc-model:dd", "source-x"},
1064+
}
1065+
1066+
for _, spec := range multiColonModels {
1067+
catalogModel := &models.CatalogModelImpl{
1068+
Attributes: &models.CatalogModelAttributes{
1069+
Name: apiutils.Of(spec.displayName),
1070+
ExternalID: apiutils.Of(spec.displayName + "-mc-ext"),
1071+
},
1072+
Properties: &[]dbmodels.Properties{
1073+
{Name: "source_id", StringValue: apiutils.Of(spec.sourceID)},
1074+
},
1075+
}
1076+
_, err := repo.Save(catalogModel)
1077+
require.NoError(t, err)
1078+
}
1079+
1080+
// Paginate through all 4 models 2 at a time; verify ordering is stable across pages.
1081+
listOptions := models.CatalogModelListOptions{
1082+
Pagination: dbmodels.Pagination{
1083+
OrderBy: apiutils.Of("NAME"),
1084+
SortOrder: apiutils.Of("ASC"),
1085+
PageSize: apiutils.Of(int32(2)),
1086+
},
1087+
}
1088+
1089+
var allFound []string
1090+
currentToken := (*string)(nil)
1091+
for range 10 {
1092+
if currentToken != nil {
1093+
listOptions.Pagination.NextPageToken = currentToken
1094+
}
1095+
page, err := repo.List(listOptions)
1096+
require.NoError(t, err)
1097+
1098+
for _, model := range page.Items {
1099+
name := *model.GetAttributes().Name
1100+
if strings.HasPrefix(name, "source-x:mc-model:") {
1101+
allFound = append(allFound, name)
1102+
}
1103+
}
1104+
1105+
if page.NextPageToken == "" || len(allFound) >= 4 {
1106+
break
1107+
}
1108+
currentToken = &page.NextPageToken
1109+
}
1110+
1111+
require.GreaterOrEqual(t, len(allFound), 4, "Should find all multi-colon models")
1112+
1113+
// Verify ASC ordering is preserved across pages
1114+
expectedOrder := []string{
1115+
"source-x:mc-model:aa",
1116+
"source-x:mc-model:bb",
1117+
"source-x:mc-model:cc",
1118+
"source-x:mc-model:dd",
1119+
}
1120+
lastIndex := -1
1121+
for _, expected := range expectedOrder {
1122+
idx := findIndex(allFound, expected)
1123+
assert.NotEqual(t, -1, idx, "Should find %s", expected)
1124+
if idx != -1 {
1125+
assert.Greater(t, idx, lastIndex, "%s should appear after previous models in ASC order", expected)
1126+
lastIndex = idx
1127+
}
1128+
}
1129+
})
1130+
10261131
t.Run("TestDeleteBySource", func(t *testing.T) {
10271132
// Setup: Create models with different source IDs
10281133
sourceID1 := "test_source_1"

catalog/internal/db/pagination/pagination.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,24 @@ func CreateNamePaginationToken(entityID int32, name *string) string {
3737
// - sortOrder: The sort order ("ASC" or "DESC")
3838
// - nextPageToken: Optional pagination token for cursor-based pagination
3939
// - pageSize: The page size (0 means no limit)
40+
// - stripSourcePrefix: When true, strips the "sourceId:" prefix before sorting.
41+
// Only models use prefixed names; artifacts and MCP servers do not.
4042
//
4143
// Returns the modified query with NAME ordering and pagination applied.
42-
func ApplyNameOrdering(query *gorm.DB, tableName string, sortOrder string, nextPageToken string, pageSize int32) *gorm.DB {
44+
func ApplyNameOrdering(query *gorm.DB, tableName string, sortOrder string, nextPageToken string, pageSize int32, stripSourcePrefix bool) *gorm.DB {
4345
// Normalize sort order
4446
order := "ASC"
4547
if sortOrder == "DESC" {
4648
order = "DESC"
4749
}
4850

49-
// Apply name-based ordering with ID as tie-breaker
50-
query = query.Order(fmt.Sprintf("%s.name %s, %s.id ASC", tableName, order, tableName))
51+
var nameExpr string
52+
if stripSourcePrefix {
53+
nameExpr = fmt.Sprintf("COALESCE(NULLIF(SUBSTRING(%s.name FROM STRPOS(%s.name, ':') + 1), ''), %s.name)", tableName, tableName, tableName)
54+
} else {
55+
nameExpr = fmt.Sprintf("%s.name", tableName)
56+
}
57+
query = query.Order(fmt.Sprintf("%s %s, %s.id ASC", nameExpr, order, tableName))
5158

5259
// Handle cursor-based pagination for NAME
5360
if nextPageToken != "" {
@@ -56,13 +63,15 @@ func ApplyNameOrdering(query *gorm.DB, tableName string, sortOrder string, nextP
5663
_ = query.AddError(fmt.Errorf("invalid nextPageToken: %w", err))
5764
return query
5865
}
59-
// Cursor pagination based on name (string comparison)
66+
// Cursor pagination based on display name (string comparison).
67+
// cursor.Value holds the display name (after the colon prefix).
6068
cmp := ">"
6169
if order == "DESC" {
6270
cmp = "<"
6371
}
64-
// Use proper string comparison with name and ID as tie-breaker
65-
query = query.Where(fmt.Sprintf("(%s.name %s ? OR (%s.name = ? AND %s.id > ?))", tableName, cmp, tableName, tableName),
72+
// Use proper string comparison with display name and ID as tie-breaker
73+
query = query.Where(
74+
fmt.Sprintf("(%s %s ? OR (%s = ? AND %s.id > ?))", nameExpr, cmp, nameExpr, tableName),
6675
cursor.Value, cursor.Value, cursor.ID)
6776
}
6877

catalog/internal/db/service/catalog_artifact.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func (r *CatalogArtifactRepositoryImpl) List(listOptions models.CatalogArtifactL
164164
// Handle NAME ordering specially (catalog-specific) to avoid string-to-integer cast issues
165165
if orderBy == "NAME" {
166166
artifactTable := utils.GetTableName(query, &schema.Artifact{})
167-
query = catpagination.ApplyNameOrdering(query, artifactTable, sortOrder, nextPageToken, pageSize)
167+
query = catpagination.ApplyNameOrdering(query, artifactTable, sortOrder, nextPageToken, pageSize, false)
168168
} else if _, isAllowedColumn := catpagination.CatalogOrderByColumns[orderBy]; isAllowedColumn {
169169
// Handle standard allowed columns (ID, CREATE_TIME, LAST_UPDATE_TIME)
170170
pagination := &dbmodels.Pagination{
@@ -383,7 +383,7 @@ func (r *CatalogArtifactRepositoryImpl) applyCustomOrdering(query *gorm.DB, list
383383

384384
// Handle NAME ordering specially (catalog-specific)
385385
if orderBy == "NAME" {
386-
return catpagination.ApplyNameOrdering(query, artifactTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize()), nil
386+
return catpagination.ApplyNameOrdering(query, artifactTable, listOptions.GetSortOrder(), listOptions.GetNextPageToken(), listOptions.GetPageSize(), false), nil
387387
}
388388

389389
subquery, sortColumn, err := r.sortValueQuery(listOptions, artifactTable+".id")

clients/ui/frontend/package-lock.json

Lines changed: 11 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/ui/frontend/src/app/pages/modelCatalog/components/HardwareConfigurationTable.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import * as React from 'react';
2-
import { DashboardEmptyTableView, Table } from 'mod-arch-shared';
2+
import { DashboardEmptyTableView, Table, ManageColumnsModal } from 'mod-arch-shared';
33
import { Button, Spinner } from '@patternfly/react-core';
44
import { ColumnsIcon } from '@patternfly/react-icons';
55
import { OuterScrollContainer } from '@patternfly/react-table';
66
import { CatalogPerformanceMetricsArtifact } from '~/app/modelCatalogTypes';
77
import { ModelCatalogContext } from '~/app/context/modelCatalog/ModelCatalogContext';
88
import { getActiveLatencyFieldName } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
9-
import { ManageColumnsModal } from '~/app/shared/components/manageColumns/ManageColumnsModal';
109
import HardwareConfigurationTableRow from './HardwareConfigurationTableRow';
1110
import HardwareConfigurationFilterToolbar from './HardwareConfigurationFilterToolbar';
1211
import { useHardwareConfigColumns, ControlledTableSortProps } from './useHardwareConfigColumns';

0 commit comments

Comments
 (0)