Skip to content

Commit 0716acc

Browse files
hide cat toogle in case only one cat is available
Signed-off-by: Philip Colares Carneiro <philip.colares@gmail.com>
1 parent bcf0562 commit 0716acc

7 files changed

Lines changed: 411 additions & 54 deletions

File tree

clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalog/modelCatalog.cy.ts

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,14 @@ describe('Performance Empty State', () => {
397397
describe('Labeled Section Without Validated Models', () => {
398398
it('should show performance empty state when toggle is ON and no validated models', () => {
399399
initIntercepts({
400-
sources: [mockCatalogSource({ labels: ['Provider one'] })],
400+
sources: [
401+
mockCatalogSource({ labels: ['Provider one'] }),
402+
mockCatalogSource({
403+
id: 'source-2',
404+
name: 'Provider Two Source',
405+
labels: ['Provider two'],
406+
}),
407+
],
401408
hasValidatedModels: false,
402409
});
403410
// No user filters/search; this scenario should hit the special "No performance data" state.
@@ -414,7 +421,14 @@ describe('Performance Empty State', () => {
414421

415422
it('should show models when toggle is OFF', () => {
416423
initIntercepts({
417-
sources: [mockCatalogSource({ labels: ['Provider one'] })],
424+
sources: [
425+
mockCatalogSource({ labels: ['Provider one'] }),
426+
mockCatalogSource({
427+
id: 'source-2',
428+
name: 'Provider Two Source',
429+
labels: ['Provider two'],
430+
}),
431+
],
418432
hasValidatedModels: false,
419433
});
420434
modelCatalog.visit();
@@ -428,7 +442,14 @@ describe('Performance Empty State', () => {
428442
describe('Labeled Section With Validated Models', () => {
429443
it('should show models when toggle is ON and section has validated models', () => {
430444
initIntercepts({
431-
sources: [mockCatalogSource({ labels: ['Provider one'] })],
445+
sources: [
446+
mockCatalogSource({ labels: ['Provider one'] }),
447+
mockCatalogSource({
448+
id: 'source-2',
449+
name: 'Provider Two Source',
450+
labels: ['Provider two'],
451+
}),
452+
],
432453
hasValidatedModels: true,
433454
});
434455
modelCatalog.visit();
@@ -485,7 +506,14 @@ describe('Performance Empty State', () => {
485506

486507
it('should show performance empty state after clicking Reset filters when toggle is ON', () => {
487508
initIntercepts({
488-
sources: [mockCatalogSource({ labels: ['Provider one'] })],
509+
sources: [
510+
mockCatalogSource({ labels: ['Provider one'] }),
511+
mockCatalogSource({
512+
id: 'source-2',
513+
name: 'Provider Two Source',
514+
labels: ['Provider two'],
515+
}),
516+
],
489517
hasValidatedModels: false,
490518
});
491519
setupFilteredModelsIntercept({ returnModelsForFilters: false });
@@ -535,7 +563,14 @@ describe('Performance Empty State', () => {
535563

536564
it('should show "No results found" when toggle is ON and user applies filter that returns 0 results', () => {
537565
initIntercepts({
538-
sources: [mockCatalogSource({ labels: ['Provider one'] })],
566+
sources: [
567+
mockCatalogSource({ labels: ['Provider one'] }),
568+
mockCatalogSource({
569+
id: 'source-2',
570+
name: 'Provider Two Source',
571+
labels: ['Provider two'],
572+
}),
573+
],
539574
hasValidatedModels: true,
540575
});
541576
setupFilteredModelsIntercept({ returnModelsForFilters: false });
@@ -556,7 +591,14 @@ describe('Performance Empty State', () => {
556591
describe('All Models Section', () => {
557592
it('should show models in All models section even when toggle is ON', () => {
558593
initIntercepts({
559-
sources: [mockCatalogSource({ labels: ['Provider one'] })],
594+
sources: [
595+
mockCatalogSource({ labels: ['Provider one'] }),
596+
mockCatalogSource({
597+
id: 'source-2',
598+
name: 'Provider Two Source',
599+
labels: ['Provider two'],
600+
}),
601+
],
560602
hasValidatedModels: true,
561603
});
562604
modelCatalog.visit();
@@ -569,3 +611,35 @@ describe('All Models Section', () => {
569611
modelCatalog.findPerformanceEmptyState().should('not.exist');
570612
});
571613
});
614+
615+
describe('Single Category Behavior', () => {
616+
it('should hide category toggle when only one category is active', () => {
617+
initIntercepts({
618+
sources: [mockCatalogSource({ labels: ['Provider one'] })],
619+
});
620+
modelCatalog.visit();
621+
622+
cy.findByTestId('label-Provider one').should('not.exist');
623+
cy.findByTestId('all').should('not.exist');
624+
});
625+
626+
it('should auto-select the single active category and show its models', () => {
627+
initIntercepts({
628+
sources: [mockCatalogSource({ labels: ['Provider one'] })],
629+
});
630+
modelCatalog.visit();
631+
632+
modelCatalog.findModelCatalogCards().should('have.length.at.least', 1);
633+
});
634+
635+
it('should show full grid view for the auto-selected single category', () => {
636+
initIntercepts({
637+
sources: [mockCatalogSource({ labels: ['Provider one'] })],
638+
hasValidatedModels: true,
639+
});
640+
modelCatalog.visit();
641+
642+
modelCatalog.togglePerformanceView();
643+
modelCatalog.findModelCatalogCards().should('have.length.at.least', 1);
644+
});
645+
});

clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalog.tsx

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
11
import * as React from 'react';
22
import { PageSection, Sidebar, SidebarContent, SidebarPanel, Stack } from '@patternfly/react-core';
33
import { ApplicationsPage, ProjectObjectType, TitleWithIcon } from 'mod-arch-shared';
4+
import { SearchIcon } from '@patternfly/react-icons';
45
import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount';
56
import { McpCatalogContext } from '~/app/context/mcpCatalog/McpCatalogContext';
67
import { hasMcpFiltersApplied } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils';
78
import McpCatalogFilters from '~/app/pages/mcpCatalog/components/McpCatalogFilters';
89
import { MCP_CATALOG_TITLE, MCP_CATALOG_DESCRIPTION } from '~/app/pages/mcpCatalog/const';
10+
import { getActiveSourceLabels } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
11+
import EmptyModelCatalogState from '~/app/pages/modelCatalog/EmptyModelCatalogState';
912
import McpCatalogSourceLabelSelector from './McpCatalogSourceLabelSelector';
1013
import McpCatalogAllServersView from './McpCatalogAllServersView';
1114
import McpCatalogGalleryView from './McpCatalogGalleryView';
1215

1316
const McpCatalog: React.FC = () => {
14-
const { searchQuery, setSearchQuery, clearAllFilters, selectedSourceLabel, filters } =
15-
React.useContext(McpCatalogContext);
17+
const {
18+
searchQuery,
19+
setSearchQuery,
20+
clearAllFilters,
21+
selectedSourceLabel,
22+
setSelectedSourceLabel,
23+
filters,
24+
catalogSources,
25+
catalogLabels,
26+
catalogSourcesLoaded,
27+
} = React.useContext(McpCatalogContext);
1628

1729
const filtersApplied = hasMcpFiltersApplied(filters, searchQuery);
1830
const isAllServersView = selectedSourceLabel === undefined && !filtersApplied;
1931

32+
const activeCategories = React.useMemo(
33+
() => getActiveSourceLabels(catalogSources, catalogLabels),
34+
[catalogSources, catalogLabels],
35+
);
36+
37+
const isSingleCategory = activeCategories.length === 1;
38+
const hasNoCategories = activeCategories.length === 0;
39+
40+
React.useEffect(() => {
41+
if (catalogSourcesLoaded && isSingleCategory) {
42+
setSelectedSourceLabel(activeCategories[0]);
43+
}
44+
}, [catalogSourcesLoaded, isSingleCategory, activeCategories, setSelectedSourceLabel]);
45+
2046
const handleSearch = React.useCallback(
2147
(term: string) => {
2248
setSearchQuery(term);
@@ -44,28 +70,37 @@ const McpCatalog: React.FC = () => {
4470
loaded
4571
provideChildrenPadding
4672
>
47-
<Sidebar hasBorder hasGutter>
48-
<SidebarPanel>
49-
<McpCatalogFilters />
50-
</SidebarPanel>
51-
<SidebarContent>
52-
<Stack hasGutter>
53-
<McpCatalogSourceLabelSelector
54-
searchTerm={searchQuery}
55-
onSearch={handleSearch}
56-
onClearSearch={handleClearSearch}
57-
onResetAllFilters={handleResetAllFilters}
58-
/>
59-
<PageSection isFilled padding={{ default: 'noPadding' }}>
60-
{isAllServersView ? (
61-
<McpCatalogAllServersView searchTerm={searchQuery} />
62-
) : (
63-
<McpCatalogGalleryView handleFilterReset={handleResetAllFilters} />
64-
)}
65-
</PageSection>
66-
</Stack>
67-
</SidebarContent>
68-
</Sidebar>
73+
{catalogSourcesLoaded && hasNoCategories ? (
74+
<EmptyModelCatalogState
75+
testid="empty-mcp-catalog-no-categories"
76+
title="No MCP servers available"
77+
headerIcon={SearchIcon}
78+
description="There are no MCP server categories available. Configure sources in settings to get started."
79+
/>
80+
) : (
81+
<Sidebar hasBorder hasGutter>
82+
<SidebarPanel>
83+
<McpCatalogFilters />
84+
</SidebarPanel>
85+
<SidebarContent>
86+
<Stack hasGutter>
87+
<McpCatalogSourceLabelSelector
88+
searchTerm={searchQuery}
89+
onSearch={handleSearch}
90+
onClearSearch={handleClearSearch}
91+
onResetAllFilters={handleResetAllFilters}
92+
/>
93+
<PageSection isFilled padding={{ default: 'noPadding' }}>
94+
{isAllServersView && !isSingleCategory ? (
95+
<McpCatalogAllServersView searchTerm={searchQuery} />
96+
) : (
97+
<McpCatalogGalleryView handleFilterReset={handleResetAllFilters} />
98+
)}
99+
</PageSection>
100+
</Stack>
101+
</SidebarContent>
102+
</Sidebar>
103+
)}
69104
</ApplicationsPage>
70105
</>
71106
);

clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpCatalogSourceLabelBlocks.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ const McpCatalogSourceLabelBlocks: React.FC = () => {
6464
return null;
6565
}
6666

67+
const activeCategoryCount = blocks.length - 1;
68+
if (activeCategoryCount <= 1) {
69+
return null;
70+
}
71+
6772
const isSelected = (block: SourceLabelBlock) =>
6873
block.label === undefined
6974
? selectedSourceLabel === undefined

clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalog.tsx

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
11
import * as React from 'react';
22
import { PageSection, Sidebar, SidebarContent, SidebarPanel } from '@patternfly/react-core';
33
import { ApplicationsPage, ProjectObjectType, TitleWithIcon } from 'mod-arch-shared';
4+
import { SearchIcon } from '@patternfly/react-icons';
45
import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount';
56
import ModelCatalogFilters from '~/app/pages/modelCatalog/components/ModelCatalogFilters';
67
import { ModelCatalogContext } from '~/app/context/modelCatalog/ModelCatalogContext';
78
import { CategoryName } from '~/app/modelCatalogTypes';
89
import { useHasVisibleFiltersApplied } from '~/app/hooks/modelCatalog/useHasVisibleFiltersApplied';
10+
import { getActiveSourceLabels } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
11+
import EmptyModelCatalogState from '~/app/pages/modelCatalog/EmptyModelCatalogState';
912
import ModelCatalogSourceLabelSelectorNavigator from './ModelCatalogSourceLabelSelectorNavigator';
1013
import ModelCatalogAllModelsView from './ModelCatalogAllModelsView';
1114
import ModelCatalogGalleryView from './ModelCatalogGalleryView';
1215

1316
const ModelCatalog: React.FC = () => {
1417
const [searchTerm, setSearchTerm] = React.useState('');
15-
const { selectedSourceLabel, clearAllFilters } = React.useContext(ModelCatalogContext);
18+
const {
19+
selectedSourceLabel,
20+
updateSelectedSourceLabel,
21+
clearAllFilters,
22+
catalogSources,
23+
catalogLabels,
24+
catalogSourcesLoaded,
25+
} = React.useContext(ModelCatalogContext);
1626
const filtersApplied = useHasVisibleFiltersApplied();
27+
28+
const activeCategories = React.useMemo(
29+
() => getActiveSourceLabels(catalogSources, catalogLabels),
30+
[catalogSources, catalogLabels],
31+
);
32+
33+
const isSingleCategory = activeCategories.length === 1;
34+
const hasNoCategories = activeCategories.length === 0;
35+
36+
React.useEffect(() => {
37+
if (catalogSourcesLoaded && isSingleCategory) {
38+
updateSelectedSourceLabel(activeCategories[0]);
39+
}
40+
}, [catalogSourcesLoaded, isSingleCategory, activeCategories, updateSelectedSourceLabel]);
41+
1742
const isAllModelsView =
1843
selectedSourceLabel === CategoryName.allModels && !searchTerm && !filtersApplied;
1944

@@ -41,29 +66,38 @@ const ModelCatalog: React.FC = () => {
4166
loaded
4267
provideChildrenPadding
4368
>
44-
<Sidebar hasBorder hasGutter>
45-
<SidebarPanel>
46-
<ModelCatalogFilters />
47-
</SidebarPanel>
48-
<SidebarContent>
49-
<ModelCatalogSourceLabelSelectorNavigator
50-
searchTerm={searchTerm}
51-
onSearch={handleSearch}
52-
onClearSearch={handleClearSearch}
53-
onResetAllFilters={handleFilterReset}
54-
/>
55-
<PageSection isFilled padding={{ default: 'noPadding' }}>
56-
{isAllModelsView ? (
57-
<ModelCatalogAllModelsView searchTerm={searchTerm} />
58-
) : (
59-
<ModelCatalogGalleryView
60-
searchTerm={searchTerm}
61-
handleFilterReset={handleFilterReset}
62-
/>
63-
)}
64-
</PageSection>
65-
</SidebarContent>
66-
</Sidebar>
69+
{catalogSourcesLoaded && hasNoCategories ? (
70+
<EmptyModelCatalogState
71+
testid="empty-model-catalog-no-categories"
72+
title="No models available"
73+
headerIcon={SearchIcon}
74+
description="There are no model categories available. Configure model sources in settings to get started."
75+
/>
76+
) : (
77+
<Sidebar hasBorder hasGutter>
78+
<SidebarPanel>
79+
<ModelCatalogFilters />
80+
</SidebarPanel>
81+
<SidebarContent>
82+
<ModelCatalogSourceLabelSelectorNavigator
83+
searchTerm={searchTerm}
84+
onSearch={handleSearch}
85+
onClearSearch={handleClearSearch}
86+
onResetAllFilters={handleFilterReset}
87+
/>
88+
<PageSection isFilled padding={{ default: 'noPadding' }}>
89+
{isAllModelsView && !isSingleCategory ? (
90+
<ModelCatalogAllModelsView searchTerm={searchTerm} />
91+
) : (
92+
<ModelCatalogGalleryView
93+
searchTerm={searchTerm}
94+
handleFilterReset={handleFilterReset}
95+
/>
96+
)}
97+
</PageSection>
98+
</SidebarContent>
99+
</Sidebar>
100+
)}
67101
</ApplicationsPage>
68102
</>
69103
);

clients/ui/frontend/src/app/pages/modelCatalog/screens/ModelCatalogSourceLabelBlocks.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ const ModelCatalogSourceLabelBlocks: React.FC = () => {
6262
return null;
6363
}
6464

65+
const activeCategoryCount = blocks.length - 1;
66+
if (activeCategoryCount <= 1) {
67+
return null;
68+
}
69+
6570
const handleToggleClick = (label: string) => {
6671
updateSelectedSourceLabel(label);
6772
};

0 commit comments

Comments
 (0)