From 7cd148bcd37dbde230b5e546d9c06b0a9ce3443c Mon Sep 17 00:00:00 2001 From: Philip Colares Carneiro Date: Wed, 15 Apr 2026 14:33:13 +0100 Subject: [PATCH 1/5] hide cat toogle in case only one cat is available Signed-off-by: Philip Colares Carneiro --- .../mocked/modelCatalog/modelCatalog.cy.ts | 86 +++++++- .../pages/mcpCatalog/screens/McpCatalog.tsx | 83 +++++--- .../screens/McpCatalogSourceLabelBlocks.tsx | 5 + .../modelCatalog/screens/ModelCatalog.tsx | 82 +++++--- .../screens/ModelCatalogSourceLabelBlocks.tsx | 5 + .../utils/__tests__/modelCatalogUtils.spec.ts | 189 ++++++++++++++++++ .../modelCatalog/utils/modelCatalogUtils.ts | 15 ++ 7 files changed, 411 insertions(+), 54 deletions(-) diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts index ad73605ed7..3f709c9e57 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts @@ -397,7 +397,14 @@ describe('Performance Empty State', () => { describe('Labeled Section Without Validated Models', () => { it('should show performance empty state when toggle is ON and no validated models', () => { initIntercepts({ - sources: [mockCatalogSource({ labels: ['Provider one'] })], + sources: [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), + ], hasValidatedModels: false, }); // No user filters/search; this scenario should hit the special "No performance data" state. @@ -414,7 +421,14 @@ describe('Performance Empty State', () => { it('should show models when toggle is OFF', () => { initIntercepts({ - sources: [mockCatalogSource({ labels: ['Provider one'] })], + sources: [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), + ], hasValidatedModels: false, }); modelCatalog.visit(); @@ -428,7 +442,14 @@ describe('Performance Empty State', () => { describe('Labeled Section With Validated Models', () => { it('should show models when toggle is ON and section has validated models', () => { initIntercepts({ - sources: [mockCatalogSource({ labels: ['Provider one'] })], + sources: [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), + ], hasValidatedModels: true, }); modelCatalog.visit(); @@ -485,7 +506,14 @@ describe('Performance Empty State', () => { it('should show performance empty state after clicking Reset filters when toggle is ON', () => { initIntercepts({ - sources: [mockCatalogSource({ labels: ['Provider one'] })], + sources: [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), + ], hasValidatedModels: false, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -535,7 +563,14 @@ describe('Performance Empty State', () => { it('should show "No results found" when toggle is ON and user applies filter that returns 0 results', () => { initIntercepts({ - sources: [mockCatalogSource({ labels: ['Provider one'] })], + sources: [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), + ], hasValidatedModels: true, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -556,7 +591,14 @@ describe('Performance Empty State', () => { describe('All Models Section', () => { it('should show models in All models section even when toggle is ON', () => { initIntercepts({ - sources: [mockCatalogSource({ labels: ['Provider one'] })], + sources: [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), + ], hasValidatedModels: true, }); modelCatalog.visit(); @@ -569,3 +611,35 @@ describe('All Models Section', () => { modelCatalog.findPerformanceEmptyState().should('not.exist'); }); }); + +describe('Single Category Behavior', () => { + it('should hide category toggle when only one category is active', () => { + initIntercepts({ + sources: [mockCatalogSource({ labels: ['Provider one'] })], + }); + modelCatalog.visit(); + + cy.findByTestId('label-Provider one').should('not.exist'); + cy.findByTestId('all').should('not.exist'); + }); + + it('should auto-select the single active category and show its models', () => { + initIntercepts({ + sources: [mockCatalogSource({ labels: ['Provider one'] })], + }); + modelCatalog.visit(); + + modelCatalog.findModelCatalogCards().should('have.length.at.least', 1); + }); + + it('should show full grid view for the auto-selected single category', () => { + initIntercepts({ + sources: [mockCatalogSource({ labels: ['Provider one'] })], + hasValidatedModels: true, + }); + modelCatalog.visit(); + + modelCatalog.togglePerformanceView(); + modelCatalog.findModelCatalogCards().should('have.length.at.least', 1); + }); +}); diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx index 5cc5522a43..6fc44bffdf 100644 --- a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx @@ -1,22 +1,48 @@ import * as React from 'react'; import { PageSection, Sidebar, SidebarContent, SidebarPanel, Stack } from '@patternfly/react-core'; import { ApplicationsPage, ProjectObjectType, TitleWithIcon } from 'mod-arch-shared'; +import { SearchIcon } from '@patternfly/react-icons'; import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount'; import { McpCatalogContext } from '~/app/context/mcpCatalog/McpCatalogContext'; import { hasMcpFiltersApplied } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; import McpCatalogFilters from '~/app/pages/mcpCatalog/components/McpCatalogFilters'; import { MCP_CATALOG_TITLE, MCP_CATALOG_DESCRIPTION } from '~/app/pages/mcpCatalog/const'; +import { getActiveSourceLabels } from '~/app/pages/modelCatalog/utils/modelCatalogUtils'; +import EmptyModelCatalogState from '~/app/pages/modelCatalog/EmptyModelCatalogState'; import McpCatalogSourceLabelSelector from './McpCatalogSourceLabelSelector'; import McpCatalogAllServersView from './McpCatalogAllServersView'; import McpCatalogGalleryView from './McpCatalogGalleryView'; const McpCatalog: React.FC = () => { - const { searchQuery, setSearchQuery, clearAllFilters, selectedSourceLabel, filters } = - React.useContext(McpCatalogContext); + const { + searchQuery, + setSearchQuery, + clearAllFilters, + selectedSourceLabel, + setSelectedSourceLabel, + filters, + catalogSources, + catalogLabels, + catalogSourcesLoaded, + } = React.useContext(McpCatalogContext); const filtersApplied = hasMcpFiltersApplied(filters, searchQuery); const isAllServersView = selectedSourceLabel === undefined && !filtersApplied; + const activeCategories = React.useMemo( + () => getActiveSourceLabels(catalogSources, catalogLabels), + [catalogSources, catalogLabels], + ); + + const isSingleCategory = activeCategories.length === 1; + const hasNoCategories = activeCategories.length === 0; + + React.useEffect(() => { + if (catalogSourcesLoaded && isSingleCategory) { + setSelectedSourceLabel(activeCategories[0]); + } + }, [catalogSourcesLoaded, isSingleCategory, activeCategories, setSelectedSourceLabel]); + const handleSearch = React.useCallback( (term: string) => { setSearchQuery(term); @@ -44,28 +70,37 @@ const McpCatalog: React.FC = () => { loaded provideChildrenPadding > - - - - - - - - - {isAllServersView ? ( - - ) : ( - - )} - - - - + {catalogSourcesLoaded && hasNoCategories ? ( + + ) : ( + + + + + + + + + {isAllServersView && !isSingleCategory ? ( + + ) : ( + + )} + + + + + )} ); diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogSourceLabelBlocks.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogSourceLabelBlocks.tsx index 69f6a38ad8..664b13beb9 100644 --- a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogSourceLabelBlocks.tsx +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogSourceLabelBlocks.tsx @@ -64,6 +64,11 @@ const McpCatalogSourceLabelBlocks: React.FC = () => { return null; } + const activeCategoryCount = blocks.length - 1; + if (activeCategoryCount <= 1) { + return null; + } + const isSelected = (block: SourceLabelBlock) => block.label === undefined ? selectedSourceLabel === undefined diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx index 528977e83a..be24293f0b 100755 --- a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx +++ b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx @@ -1,19 +1,44 @@ import * as React from 'react'; import { PageSection, Sidebar, SidebarContent, SidebarPanel } from '@patternfly/react-core'; import { ApplicationsPage, ProjectObjectType, TitleWithIcon } from 'mod-arch-shared'; +import { SearchIcon } from '@patternfly/react-icons'; import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount'; import ModelCatalogFilters from '~/app/pages/modelCatalog/components/ModelCatalogFilters'; import { ModelCatalogContext } from '~/app/context/modelCatalog/ModelCatalogContext'; import { CategoryName } from '~/app/modelCatalogTypes'; import { useHasVisibleFiltersApplied } from '~/app/hooks/modelCatalog/useHasVisibleFiltersApplied'; +import { getActiveSourceLabels } from '~/app/pages/modelCatalog/utils/modelCatalogUtils'; +import EmptyModelCatalogState from '~/app/pages/modelCatalog/EmptyModelCatalogState'; import ModelCatalogSourceLabelSelectorNavigator from './ModelCatalogSourceLabelSelectorNavigator'; import ModelCatalogAllModelsView from './ModelCatalogAllModelsView'; import ModelCatalogGalleryView from './ModelCatalogGalleryView'; const ModelCatalog: React.FC = () => { const [searchTerm, setSearchTerm] = React.useState(''); - const { selectedSourceLabel, clearAllFilters } = React.useContext(ModelCatalogContext); + const { + selectedSourceLabel, + updateSelectedSourceLabel, + clearAllFilters, + catalogSources, + catalogLabels, + catalogSourcesLoaded, + } = React.useContext(ModelCatalogContext); const filtersApplied = useHasVisibleFiltersApplied(); + + const activeCategories = React.useMemo( + () => getActiveSourceLabels(catalogSources, catalogLabels), + [catalogSources, catalogLabels], + ); + + const isSingleCategory = activeCategories.length === 1; + const hasNoCategories = activeCategories.length === 0; + + React.useEffect(() => { + if (catalogSourcesLoaded && isSingleCategory) { + updateSelectedSourceLabel(activeCategories[0]); + } + }, [catalogSourcesLoaded, isSingleCategory, activeCategories, updateSelectedSourceLabel]); + const isAllModelsView = selectedSourceLabel === CategoryName.allModels && !searchTerm && !filtersApplied; @@ -41,29 +66,38 @@ const ModelCatalog: React.FC = () => { loaded provideChildrenPadding > - - - - - - - - {isAllModelsView ? ( - - ) : ( - - )} - - - + {catalogSourcesLoaded && hasNoCategories ? ( + + ) : ( + + + + + + + + {isAllModelsView && !isSingleCategory ? ( + + ) : ( + + )} + + + + )} ); diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogSourceLabelBlocks.tsx b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogSourceLabelBlocks.tsx index 47a608d466..1c16d6ac4e 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogSourceLabelBlocks.tsx +++ b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogSourceLabelBlocks.tsx @@ -62,6 +62,11 @@ const ModelCatalogSourceLabelBlocks: React.FC = () => { return null; } + const activeCategoryCount = blocks.length - 1; + if (activeCategoryCount <= 1) { + return null; + } + const handleToggleClick = (label: string) => { updateSelectedSourceLabel(label); }; diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts b/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts index 99629039ad..2624648cb8 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts +++ b/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts @@ -32,6 +32,7 @@ import { hasFiltersApplied, getArchitecturesFromArtifacts, getModelName, + getActiveSourceLabels, } from '~/app/pages/modelCatalog/utils/modelCatalogUtils'; import { mockCatalogModelArtifact } from '~/__mocks__/mockCatalogModelArtifactList'; import { ModelRegistryMetadataType } from '~/app/types'; @@ -1383,3 +1384,191 @@ describe('getModelName', () => { expect(result).toBe('my-model_v1'); }); }); + +describe('getActiveSourceLabels', () => { + const createSource = (overrides: Partial = {}): CatalogSource => ({ + id: 'source-1', + name: 'Test Source', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + ...overrides, + }); + + const createSourceList = (items: CatalogSource[] = []): CatalogSourceList => ({ + items, + size: items.length, + pageSize: 10, + nextPageToken: '', + }); + + it('returns empty array when catalogSources is null', () => { + expect(getActiveSourceLabels(null, null)).toEqual([]); + }); + + it('returns empty array when no sources are enabled or available', () => { + const sources = createSourceList([ + createSource({ + id: '1', + enabled: false, + status: CatalogSourceStatus.DISABLED, + }), + createSource({ + id: '2', + enabled: true, + status: CatalogSourceStatus.ERROR, + }), + ]); + expect(getActiveSourceLabels(sources, null)).toEqual([]); + }); + + it('returns a single label when only one category exists', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + expect(getActiveSourceLabels(sources, null)).toEqual(['Red Hat']); + }); + + it('returns multiple labels when multiple categories exist', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: ['Community'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toHaveLength(2); + expect(result).toContain('Red Hat'); + expect(result).toContain('Community'); + }); + + it('includes "null" label for sources without labels', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: [], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toContain('Red Hat'); + expect(result).toContain('null'); + }); + + it('excludes disabled sources from active categories', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: ['Excluded'], + enabled: false, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toEqual(['Red Hat']); + }); + + it('excludes sources with error status', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: ['Error Source'], + enabled: true, + status: CatalogSourceStatus.ERROR, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toEqual(['Red Hat']); + }); + + it('includes partially-available sources', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: ['Partial'], + enabled: true, + status: CatalogSourceStatus.PARTIALLY_AVAILABLE, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toHaveLength(2); + expect(result).toContain('Red Hat'); + expect(result).toContain('Partial'); + }); + + it('deduplicates labels from multiple sources with the same label', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toEqual(['Red Hat']); + }); + + it('returns only "null" label when all sources have no labels', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: [], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: [], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + const result = getActiveSourceLabels(sources, null); + expect(result).toEqual(['null']); + }); +}); diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/utils/modelCatalogUtils.ts b/clients/ui/frontend/src/app/pages/modelCatalog/utils/modelCatalogUtils.ts index 02c755794c..133c8d4e1d 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalog/utils/modelCatalogUtils.ts +++ b/clients/ui/frontend/src/app/pages/modelCatalog/utils/modelCatalogUtils.ts @@ -750,6 +750,21 @@ export const orderLabelsByPriority = ( return orderedLabels; }; +export const getActiveSourceLabels = ( + catalogSources: CatalogSourceList | null, + catalogLabels: CatalogLabelList | null, +): string[] => { + const enabledSources = filterEnabledCatalogSources(catalogSources); + const uniqueLabels = getUniqueSourceLabels(enabledSources); + const orderedLabels = orderLabelsByPriority(uniqueLabels, catalogLabels); + + if (hasSourcesWithoutLabels(enabledSources)) { + return [...orderedLabels, SourceLabel.other]; + } + + return orderedLabels; +}; + /** * Formats model type value for display in the UI. * Converts raw API values (generative, predictive, unknown) to user-friendly display labels. From a6240d5216f96a561b99791e8de73f0906ed6a0f Mon Sep 17 00:00:00 2001 From: Philip Colares Carneiro Date: Mon, 20 Apr 2026 08:40:42 +0100 Subject: [PATCH 2/5] fix changes requested in tests Signed-off-by: Philip Colares Carneiro --- .../src/__mocks__/mockCatalogSourceList.ts | 19 ++++ .../mocked/modelCatalog/modelCatalog.cy.ts | 106 ++++-------------- 2 files changed, 39 insertions(+), 86 deletions(-) diff --git a/clients/ui/frontend/src/__mocks__/mockCatalogSourceList.ts b/clients/ui/frontend/src/__mocks__/mockCatalogSourceList.ts index 070c296764..809ddf6ae3 100644 --- a/clients/ui/frontend/src/__mocks__/mockCatalogSourceList.ts +++ b/clients/ui/frontend/src/__mocks__/mockCatalogSourceList.ts @@ -56,6 +56,25 @@ export const mockCatalogSourceActive = (): CatalogSource => ({ status: 'available', }); +export const mockTwoProviderSources = (): CatalogSource[] => [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ + id: 'source-2', + name: 'Provider Two Source', + labels: ['Provider two'], + }), +]; + +export const mockProviderAndCustomSources = (): CatalogSource[] => [ + mockCatalogSource({ labels: ['Provider one'] }), + mockCatalogSource({ id: 'custom-source', name: 'Custom Source', labels: [] }), +]; + +export const mockDefaultSources = (): CatalogSource[] => [ + mockCatalogSource({}), + mockCatalogSource({ id: 'source-2', name: 'source 2' }), +]; + export const mockCatalogSourceList = (partial?: Partial): CatalogSourceList => ({ items: [ mockCatalogSourceActive(), diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts index 3f709c9e57..d04b853869 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts @@ -10,11 +10,13 @@ import { mockCatalogPerformanceMetricsArtifact, mockCatalogSource, mockCatalogSourceList, + mockDefaultSources, + mockProviderAndCustomSources, + mockTwoProviderSources, } from '~/__mocks__'; -import type { CatalogSource } from '~/app/modelCatalogTypes'; import { MODEL_CATALOG_API_VERSION } from '~/__tests__/cypress/cypress/support/commands/api'; import { mockCatalogFilterOptionsList } from '~/__mocks__/mockCatalogFilterOptionsList'; -import { SourceLabel } from '~/app/modelCatalogTypes'; +import { SourceLabel, type CatalogSource } from '~/app/modelCatalogTypes'; import { ModelRegistryMetadataType } from '~/app/types'; type FilteredModelsInterceptConfig = { @@ -90,7 +92,7 @@ const calculateExpectedCategoryCount = (sources: CatalogSource[]): number => { }; const initIntercepts = ({ - sources = [mockCatalogSource({}), mockCatalogSource({ id: 'source-2', name: 'source 2' })], + sources = mockDefaultSources(), modelsPerCategory = 4, hasValidatedModels = false, includeAllModelsIntercept = true, @@ -269,15 +271,9 @@ describe('Model Catalog Page', () => { }); it('checkbox should work', () => { - // Calculate expected category count based on sources - const defaultSources = [ - mockCatalogSource({}), - mockCatalogSource({ id: 'source-2', name: 'source 2' }), - ]; - - const expectedCategoryCount = calculateExpectedCategoryCount(defaultSources); + const expectedCategoryCount = calculateExpectedCategoryCount(mockDefaultSources()); - initIntercepts({ sources: defaultSources, includeAllModelsIntercept: false }); + initIntercepts({ sources: mockDefaultSources(), includeAllModelsIntercept: false }); setupFilteredModelsIntercept({ returnModelsForFilters: true, @@ -322,14 +318,9 @@ describe('Model Catalog Page', () => { }); it('tensor type filter combined with other filters should work', () => { - const defaultSources = [ - mockCatalogSource({}), - mockCatalogSource({ id: 'source-2', name: 'source 2' }), - ]; + const expectedCategoryCount = calculateExpectedCategoryCount(mockDefaultSources()); - const expectedCategoryCount = calculateExpectedCategoryCount(defaultSources); - - initIntercepts({ sources: defaultSources, includeAllModelsIntercept: false }); + initIntercepts({ sources: mockDefaultSources(), includeAllModelsIntercept: false }); setupFilteredModelsIntercept({ returnModelsForFilters: true, @@ -357,10 +348,7 @@ describe('Performance Empty State', () => { describe('Community & Custom Section', () => { it('should show performance empty state when toggle is ON', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ id: 'custom-source', name: 'Custom Source', labels: [] }), - ], + sources: mockProviderAndCustomSources(), hasValidatedModels: true, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -380,10 +368,7 @@ describe('Performance Empty State', () => { it('should show models when toggle is OFF', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ id: 'custom-source', name: 'Custom Source', labels: [] }), - ], + sources: mockProviderAndCustomSources(), }); modelCatalog.visit(); @@ -397,14 +382,7 @@ describe('Performance Empty State', () => { describe('Labeled Section Without Validated Models', () => { it('should show performance empty state when toggle is ON and no validated models', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ - id: 'source-2', - name: 'Provider Two Source', - labels: ['Provider two'], - }), - ], + sources: mockTwoProviderSources(), hasValidatedModels: false, }); // No user filters/search; this scenario should hit the special "No performance data" state. @@ -421,14 +399,7 @@ describe('Performance Empty State', () => { it('should show models when toggle is OFF', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ - id: 'source-2', - name: 'Provider Two Source', - labels: ['Provider two'], - }), - ], + sources: mockTwoProviderSources(), hasValidatedModels: false, }); modelCatalog.visit(); @@ -442,14 +413,7 @@ describe('Performance Empty State', () => { describe('Labeled Section With Validated Models', () => { it('should show models when toggle is ON and section has validated models', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ - id: 'source-2', - name: 'Provider Two Source', - labels: ['Provider two'], - }), - ], + sources: mockTwoProviderSources(), hasValidatedModels: true, }); modelCatalog.visit(); @@ -465,10 +429,7 @@ describe('Performance Empty State', () => { describe('Empty State Actions', () => { it('should turn off toggle when clicking "Turn Model performance view off"', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ id: 'custom-source', name: 'Custom Source', labels: [] }), - ], + sources: mockProviderAndCustomSources(), hasValidatedModels: true, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -486,10 +447,7 @@ describe('Performance Empty State', () => { it('should navigate to All models when clicking "View all models with performance data"', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ id: 'custom-source', name: 'Custom Source', labels: [] }), - ], + sources: mockProviderAndCustomSources(), hasValidatedModels: true, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -506,14 +464,7 @@ describe('Performance Empty State', () => { it('should show performance empty state after clicking Reset filters when toggle is ON', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ - id: 'source-2', - name: 'Provider Two Source', - labels: ['Provider two'], - }), - ], + sources: mockTwoProviderSources(), hasValidatedModels: false, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -536,10 +487,7 @@ describe('Performance Empty State', () => { it('should work correctly when toggling performance view', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ id: 'custom-source', name: 'Custom Source', labels: [] }), - ], + sources: mockProviderAndCustomSources(), hasValidatedModels: true, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -563,14 +511,7 @@ describe('Performance Empty State', () => { it('should show "No results found" when toggle is ON and user applies filter that returns 0 results', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ - id: 'source-2', - name: 'Provider Two Source', - labels: ['Provider two'], - }), - ], + sources: mockTwoProviderSources(), hasValidatedModels: true, }); setupFilteredModelsIntercept({ returnModelsForFilters: false }); @@ -591,14 +532,7 @@ describe('Performance Empty State', () => { describe('All Models Section', () => { it('should show models in All models section even when toggle is ON', () => { initIntercepts({ - sources: [ - mockCatalogSource({ labels: ['Provider one'] }), - mockCatalogSource({ - id: 'source-2', - name: 'Provider Two Source', - labels: ['Provider two'], - }), - ], + sources: mockTwoProviderSources(), hasValidatedModels: true, }); modelCatalog.visit(); From b2d9a83fe836ba45cb6f47ac2581fd9d3f589892 Mon Sep 17 00:00:00 2001 From: Philip Colares Carneiro Date: Mon, 20 Apr 2026 09:39:27 +0100 Subject: [PATCH 3/5] fix changes requested Signed-off-by: Philip Colares Carneiro --- .../pages/mcpCatalog/screens/McpCatalog.tsx | 5 +- .../screens/McpCatalogGalleryView.tsx | 50 +++++++++++++++++-- .../modelCatalog/screens/ModelCatalog.tsx | 1 + .../screens/ModelCatalogGalleryView.tsx | 25 ++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx index 6fc44bffdf..14da315b58 100644 --- a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx @@ -94,7 +94,10 @@ const McpCatalog: React.FC = () => { {isAllServersView && !isSingleCategory ? ( ) : ( - + )} diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx index 73920a9586..0bf37dd777 100644 --- a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx @@ -3,6 +3,7 @@ import { Alert, Bullseye, Button, + Content, EmptyState, Flex, Grid, @@ -13,8 +14,15 @@ import { import { SearchIcon } from '@patternfly/react-icons'; import { McpCatalogContext } from '~/app/context/mcpCatalog/McpCatalogContext'; import { useMcpServersBySourceLabelWithAPI } from '~/app/hooks/mcpServerCatalog/useMcpServersBySourceLabel'; -import { MCP_CATALOG_GRID_SPAN } from '~/app/pages/mcpCatalog/const'; +import { + MCP_CATALOG_GRID_SPAN, + OTHER_MCP_SERVERS_DISPLAY_NAME, +} from '~/app/pages/mcpCatalog/const'; import { mcpFiltersToFilterQuery } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils'; +import { + getLabelDisplayName, + getLabelDescription, +} from '~/app/pages/modelCatalog/utils/modelCatalogUtils'; import EmptyModelCatalogState from '~/app/pages/modelCatalog/EmptyModelCatalogState'; import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount'; import McpCatalogCard from '~/app/pages/mcpCatalog/components/McpCatalogCard'; @@ -23,11 +31,21 @@ const PAGE_SIZE = 10; type McpCatalogGalleryViewProps = { handleFilterReset: () => void; + isSingleCategory?: boolean; }; -const McpCatalogGalleryView: React.FC = ({ handleFilterReset }) => { - const { mcpApiState, selectedSourceLabel, searchQuery, filters, catalogLabelsLoaded } = - React.useContext(McpCatalogContext); +const McpCatalogGalleryView: React.FC = ({ + handleFilterReset, + isSingleCategory = false, +}) => { + const { + mcpApiState, + selectedSourceLabel, + searchQuery, + filters, + catalogLabels, + catalogLabelsLoaded, + } = React.useContext(McpCatalogContext); const filterQuery = React.useMemo(() => mcpFiltersToFilterQuery(filters), [filters]); @@ -80,9 +98,33 @@ const McpCatalogGalleryView: React.FC = ({ handleFil ); } + const categoryTitle = isSingleCategory + ? getLabelDisplayName( + selectedSourceLabel || '', + catalogLabels, + OTHER_MCP_SERVERS_DISPLAY_NAME, + 'servers', + ) + : undefined; + const categoryDescription = isSingleCategory + ? getLabelDescription(selectedSourceLabel || '', catalogLabels) + : undefined; + return ( <> + {isSingleCategory && categoryTitle && ( +
+ + {categoryTitle} + + {categoryDescription && ( + + {categoryDescription} + + )} +
+ )} {items.map((server) => ( { )} diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx index f08c7a8f5f..35e23ce7f7 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx +++ b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx @@ -2,6 +2,7 @@ import { Alert, Bullseye, Button, + Content, EmptyState, EmptyStateVariant, Flex, @@ -22,6 +23,8 @@ import { getActiveLatencyFieldName, getSortParams, generateCategoryName, + getLabelDisplayName, + getLabelDescription, hasFiltersApplied, isValueDifferentFromDefault, } from '~/app/pages/modelCatalog/utils/modelCatalogUtils'; @@ -38,11 +41,13 @@ import { type ModelCatalogPageProps = { searchTerm: string; handleFilterReset: () => void; + isSingleCategory?: boolean; }; const ModelCatalogGalleryView: React.FC = ({ searchTerm, handleFilterReset, + isSingleCategory = false, }) => { const { selectedSourceLabel, @@ -51,6 +56,7 @@ const ModelCatalogGalleryView: React.FC = ({ filterOptionsLoaded, filterOptionsLoadError, catalogSources, + catalogLabels, catalogLabelsLoaded, catalogLabelsLoadError, setPerformanceViewEnabled, @@ -270,9 +276,28 @@ const ModelCatalogGalleryView: React.FC = ({ ); } + const categoryTitle = isSingleCategory + ? getLabelDisplayName(selectedSourceLabel || '', catalogLabels) + : undefined; + const categoryDescription = isSingleCategory + ? getLabelDescription(selectedSourceLabel || '', catalogLabels) + : undefined; + return ( <> + {isSingleCategory && categoryTitle && ( +
+ + {categoryTitle} + + {categoryDescription && ( + + {categoryDescription} + + )} +
+ )} {catalogModels.items.map((model: CatalogModel) => ( From 32a35dbba74aa2195f0910c0ea3071b288877b47 Mon Sep 17 00:00:00 2001 From: Philip Colares Carneiro Date: Wed, 22 Apr 2026 12:29:37 +0100 Subject: [PATCH 4/5] add changes requested Signed-off-by: Philip Colares Carneiro --- .../modelCatalog/screens/ModelCatalog.tsx | 11 ++++- .../screens/ModelCatalogGalleryView.tsx | 7 +++- .../utils/__tests__/modelCatalogUtils.spec.ts | 40 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx index b0d853a857..14fbc50ac5 100755 --- a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx +++ b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx @@ -34,10 +34,16 @@ const ModelCatalog: React.FC = () => { const hasNoCategories = activeCategories.length === 0; React.useEffect(() => { - if (catalogSourcesLoaded && isSingleCategory) { + if (catalogSourcesLoaded && isSingleCategory && selectedSourceLabel !== activeCategories[0]) { updateSelectedSourceLabel(activeCategories[0]); } - }, [catalogSourcesLoaded, isSingleCategory, activeCategories, updateSelectedSourceLabel]); + }, [ + catalogSourcesLoaded, + isSingleCategory, + activeCategories, + selectedSourceLabel, + updateSelectedSourceLabel, + ]); const isAllModelsView = selectedSourceLabel === CategoryName.allModels && !searchTerm && !filtersApplied; @@ -93,6 +99,7 @@ const ModelCatalog: React.FC = () => { searchTerm={searchTerm} handleFilterReset={handleFilterReset} isSingleCategory={isSingleCategory} + singleCategoryLabel={isSingleCategory ? activeCategories[0] : undefined} /> )} diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx index 35e23ce7f7..ac616fe0f5 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx +++ b/clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogGalleryView.tsx @@ -42,12 +42,14 @@ type ModelCatalogPageProps = { searchTerm: string; handleFilterReset: () => void; isSingleCategory?: boolean; + singleCategoryLabel?: string; }; const ModelCatalogGalleryView: React.FC = ({ searchTerm, handleFilterReset, isSingleCategory = false, + singleCategoryLabel, }) => { const { selectedSourceLabel, @@ -276,11 +278,12 @@ const ModelCatalogGalleryView: React.FC = ({ ); } + const effectiveCategoryLabel = singleCategoryLabel || selectedSourceLabel || ''; const categoryTitle = isSingleCategory - ? getLabelDisplayName(selectedSourceLabel || '', catalogLabels) + ? getLabelDisplayName(effectiveCategoryLabel, catalogLabels) : undefined; const categoryDescription = isSingleCategory - ? getLabelDescription(selectedSourceLabel || '', catalogLabels) + ? getLabelDescription(effectiveCategoryLabel, catalogLabels) : undefined; return ( diff --git a/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts b/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts index 2624648cb8..beaa330722 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts +++ b/clients/ui/frontend/src/app/pages/modelCatalog/utils/__tests__/modelCatalogUtils.spec.ts @@ -2,6 +2,8 @@ /* eslint-disable camelcase */ import { CatalogFilterOptionsList, + CatalogLabel, + CatalogLabelList, CatalogSource, CatalogSourceList, ModelCatalogFilterStates, @@ -1571,4 +1573,42 @@ describe('getActiveSourceLabels', () => { const result = getActiveSourceLabels(sources, null); expect(result).toEqual(['null']); }); + + it('orders labels by catalogLabels priority when catalogLabels is provided', () => { + const sources = createSourceList([ + createSource({ + id: '1', + labels: ['Community'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '2', + labels: ['Red Hat'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + createSource({ + id: '3', + labels: ['Partner'], + enabled: true, + status: CatalogSourceStatus.AVAILABLE, + }), + ]); + + const createLabel = (name: string | null): CatalogLabel => ({ + name, + displayName: name ?? undefined, + }); + + const catalogLabels: CatalogLabelList = { + items: [createLabel('Red Hat'), createLabel('Partner'), createLabel('Community')], + size: 3, + pageSize: 10, + nextPageToken: '', + }; + + const result = getActiveSourceLabels(sources, catalogLabels); + expect(result).toEqual(['Red Hat', 'Partner', 'Community']); + }); }); From 54cbbd8657f8f7c2039762ce5c868969fd20fd77 Mon Sep 17 00:00:00 2001 From: Philip Colares Carneiro Date: Wed, 22 Apr 2026 14:09:31 +0100 Subject: [PATCH 5/5] add changes requested Signed-off-by: Philip Colares Carneiro --- .../src/app/pages/mcpCatalog/screens/McpCatalog.tsx | 11 +++++++++-- .../mcpCatalog/screens/McpCatalogGalleryView.tsx | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx index 14da315b58..7dee8bf365 100644 --- a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx @@ -38,10 +38,16 @@ const McpCatalog: React.FC = () => { const hasNoCategories = activeCategories.length === 0; React.useEffect(() => { - if (catalogSourcesLoaded && isSingleCategory) { + if (catalogSourcesLoaded && isSingleCategory && selectedSourceLabel !== activeCategories[0]) { setSelectedSourceLabel(activeCategories[0]); } - }, [catalogSourcesLoaded, isSingleCategory, activeCategories, setSelectedSourceLabel]); + }, [ + catalogSourcesLoaded, + isSingleCategory, + activeCategories, + selectedSourceLabel, + setSelectedSourceLabel, + ]); const handleSearch = React.useCallback( (term: string) => { @@ -97,6 +103,7 @@ const McpCatalog: React.FC = () => { )} diff --git a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx index 0bf37dd777..e34ef8cd38 100644 --- a/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx +++ b/clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogGalleryView.tsx @@ -32,11 +32,13 @@ const PAGE_SIZE = 10; type McpCatalogGalleryViewProps = { handleFilterReset: () => void; isSingleCategory?: boolean; + singleCategoryLabel?: string; }; const McpCatalogGalleryView: React.FC = ({ handleFilterReset, isSingleCategory = false, + singleCategoryLabel, }) => { const { mcpApiState, @@ -98,16 +100,17 @@ const McpCatalogGalleryView: React.FC = ({ ); } + const effectiveCategoryLabel = singleCategoryLabel || selectedSourceLabel || ''; const categoryTitle = isSingleCategory ? getLabelDisplayName( - selectedSourceLabel || '', + effectiveCategoryLabel, catalogLabels, OTHER_MCP_SERVERS_DISPLAY_NAME, 'servers', ) : undefined; const categoryDescription = isSingleCategory - ? getLabelDescription(selectedSourceLabel || '', catalogLabels) + ? getLabelDescription(effectiveCategoryLabel, catalogLabels) : undefined; return (