Skip to content

Commit f7d0994

Browse files
authored
Refactor use case filter to use multi-select instead of single-select (#1854)
* Refactor use case filter to use multi-select filter Signed-off-by: manaswinidas <dasmanaswini10@gmail.com> * Mock data changes Signed-off-by: manaswinidas <dasmanaswini10@gmail.com> * Address review comments Signed-off-by: manaswinidas <dasmanaswini10@gmail.com> * Cleanup and unit test fixes Signed-off-by: manaswinidas <dasmanaswini10@gmail.com> * Fix Cypress tests Signed-off-by: manaswinidas <dasmanaswini10@gmail.com> --------- Signed-off-by: manaswinidas <dasmanaswini10@gmail.com>
1 parent 6942727 commit f7d0994

16 files changed

Lines changed: 389 additions & 162 deletions

clients/ui/bff/internal/mocks/model_catalog_client_mock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func (m *ModelCatalogClientMock) GetCatalogModelArtifacts(client httpclient.HTTP
172172
var allMockModelArtifacts models.CatalogModelArtifactList
173173

174174
if sourceId == "sample-source" && modelName == "repo1%2Fgranite-8b-code-instruct" {
175-
performanceArtifacts := GetCatalogPerformanceMetricsArtifactListMock(3)
175+
performanceArtifacts := GetCatalogPerformanceMetricsArtifactListMock(4)
176176
accuracyArtifacts := GetCatalogAccuracyMetricsArtifactListMock()
177177
modelArtifacts := GetCatalogModelArtifactListMock()
178178
combinedItems := append(performanceArtifacts.Items, accuracyArtifacts.Items...)

clients/ui/bff/internal/mocks/static_data_mock.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,44 @@ func GetCatalogPerformanceMetricsArtifactMock(itemCount int32) []models.CatalogA
11561156
},
11571157
}),
11581158
},
1159+
{
1160+
ArtifactType: *stringToPointer("metrics-artifact"),
1161+
MetricsType: stringToPointer("performance-metrics"),
1162+
CreateTimeSinceEpoch: stringToPointer("1693526400000"),
1163+
LastUpdateTimeSinceEpoch: stringToPointer("1704067200000"),
1164+
CustomProperties: performanceMetricsCustomProperties(map[string]openapi.MetadataValue{
1165+
"hardware_type": {
1166+
MetadataStringValue: &openapi.MetadataStringValue{
1167+
StringValue: "A100",
1168+
MetadataType: "MetadataStringValue",
1169+
},
1170+
},
1171+
"hardware_count": {
1172+
MetadataIntValue: &openapi.MetadataIntValue{
1173+
IntValue: "8",
1174+
MetadataType: "MetadataIntValue",
1175+
},
1176+
},
1177+
"requests_per_second": {
1178+
MetadataDoubleValue: &openapi.MetadataDoubleValue{
1179+
DoubleValue: 25,
1180+
MetadataType: "MetadataDoubleValue",
1181+
},
1182+
},
1183+
"ttft_mean": {
1184+
MetadataDoubleValue: &openapi.MetadataDoubleValue{
1185+
DoubleValue: 28.5,
1186+
MetadataType: "MetadataDoubleValue",
1187+
},
1188+
},
1189+
"use_case": {
1190+
MetadataStringValue: &openapi.MetadataStringValue{
1191+
StringValue: "long_rag",
1192+
MetadataType: "MetadataStringValue",
1193+
},
1194+
},
1195+
}),
1196+
},
11591197
}
11601198
artifacts = artifacts[:itemCount]
11611199
return artifacts
@@ -1256,6 +1294,22 @@ func GetFilterOptionMocks() map[string]models.FilterOption {
12561294
},
12571295
}
12581296

1297+
// String type filter for use cases
1298+
filterOptions["use_case"] = models.FilterOption{
1299+
Type: FilterOptionTypeString,
1300+
Values: []interface{}{
1301+
"chatbot", "code_fixing", "long_rag", "rag",
1302+
},
1303+
}
1304+
1305+
// String type filter for use cases
1306+
filterOptions["use_case"] = models.FilterOption{
1307+
Type: FilterOptionTypeString,
1308+
Values: []interface{}{
1309+
"chatbot", "code_fixing", "long_rag", "rag",
1310+
},
1311+
}
1312+
12591313
filterOptions["ttft_mean"] = models.FilterOption{
12601314
Type: FilterOptionTypeNumber,
12611315
Range: &models.FilterRange{

clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelCatalog.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ class ModelCatalog {
267267
}
268268

269269
findWorkloadTypeOption(label: string) {
270-
return this.findWorkloadTypeFilter().findDropdownItem(label);
270+
// Workload type uses checkboxes in a panel, not menu items
271+
// Find checkbox by its label within the dropdown panel
272+
return cy.contains('label', label).parent().find('input[type="checkbox"]');
271273
}
272274

273275
selectWorkloadType(label: string) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ describe('Model Catalog Page', () => {
162162
]).then((interceptions) => {
163163
const lastInterception = interceptions[interceptions.length - 1];
164164
expect(lastInterception.request.url).to.include(
165-
'%28tasks+LIKE+%27%25%22text-generation%22%25%27+OR+tasks+LIKE+%27%25%22text-to-text%22%25%27%29+AND+provider%3D%27Google%27',
165+
'tasks+IN+%28%27text-generation%27%2C%27text-to-text%27%29+AND+provider%3D%27Google%27',
166166
);
167167
});
168168
});

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ describe('Model Catalog Details Tabs', () => {
226226
it('should show workload type options when clicked', () => {
227227
modelCatalog.findModelCatalogDetailLink().first().click();
228228
modelCatalog.clickPerformanceInsightsTab();
229+
modelCatalog.findWorkloadTypeFilter().click();
229230
modelCatalog.findWorkloadTypeOption('Chatbot').should('be.visible');
230231
modelCatalog.findWorkloadTypeOption('Code Fixing').should('be.visible');
231232
modelCatalog.findWorkloadTypeOption('Long RAG').should('be.visible');
@@ -235,17 +236,19 @@ describe('Model Catalog Details Tabs', () => {
235236
it('should update toggle text when workload type is selected', () => {
236237
modelCatalog.findModelCatalogDetailLink().first().click();
237238
modelCatalog.clickPerformanceInsightsTab();
239+
modelCatalog.findWorkloadTypeFilter().click();
238240
modelCatalog.selectWorkloadType('Code Fixing');
239241
modelCatalog
240242
.findWorkloadTypeFilter()
241-
.should('contain.text', 'Workload type:')
242-
.should('contain.text', 'Code Fixing');
243+
.should('contain.text', 'Workload type')
244+
.should('contain.text', '1 selected');
243245
});
244246

245247
it('should filter hardware configuration table by selected workload type', () => {
246248
modelCatalog.findModelCatalogDetailLink().first().click();
247249
modelCatalog.clickPerformanceInsightsTab();
248250
modelCatalog.findHardwareConfigurationTableRows().should('have.length.at.least', 1);
251+
modelCatalog.findWorkloadTypeFilter().click();
249252
modelCatalog.selectWorkloadType('Code Fixing');
250253
modelCatalog.findHardwareConfigurationTableRows().should('exist');
251254
modelCatalog.findHardwareConfigurationColumn('Workload type').each(($el) => {
@@ -256,15 +259,16 @@ describe('Model Catalog Details Tabs', () => {
256259
it('should clear workload type filter when clicking selected option again', () => {
257260
modelCatalog.findModelCatalogDetailLink().first().click();
258261
modelCatalog.clickPerformanceInsightsTab();
262+
modelCatalog.findWorkloadTypeFilter().click();
259263
modelCatalog.selectWorkloadType('Code Fixing');
260264
modelCatalog
261265
.findWorkloadTypeFilter()
262-
.should('contain.text', 'Workload type:')
263-
.should('contain.text', 'Code Fixing');
266+
.should('contain.text', 'Workload type')
267+
.should('contain.text', '1 selected');
264268

265269
modelCatalog.selectWorkloadType('Code Fixing');
266270
modelCatalog.findWorkloadTypeFilter().should('contain.text', 'Workload type');
267-
modelCatalog.findWorkloadTypeFilter().should('not.contain.text', 'Code Fixing');
271+
modelCatalog.findWorkloadTypeFilter().should('not.contain.text', '1 selected');
268272
});
269273
});
270274

clients/ui/frontend/src/app/context/modelCatalog/ModelCatalogContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const ModelCatalogContext = React.createContext<ModelCatalogContextType>(
5454
[ModelCatalogStringFilterKey.LICENSE]: [],
5555
[ModelCatalogStringFilterKey.LANGUAGE]: [],
5656
[ModelCatalogStringFilterKey.HARDWARE_TYPE]: [],
57-
[ModelCatalogStringFilterKey.USE_CASE]: undefined,
57+
[ModelCatalogStringFilterKey.USE_CASE]: [],
5858
[ModelCatalogNumberFilterKey.MIN_RPS]: undefined,
5959
},
6060
updateSelectedSource: () => undefined,
@@ -85,7 +85,7 @@ export const ModelCatalogContextProvider: React.FC<ModelCatalogContextProviderPr
8585
[ModelCatalogStringFilterKey.LICENSE]: [],
8686
[ModelCatalogStringFilterKey.LANGUAGE]: [],
8787
[ModelCatalogStringFilterKey.HARDWARE_TYPE]: [],
88-
[ModelCatalogStringFilterKey.USE_CASE]: undefined,
88+
[ModelCatalogStringFilterKey.USE_CASE]: [],
8989
[ModelCatalogNumberFilterKey.MIN_RPS]: undefined,
9090
});
9191
const [filterOptions, filterOptionsLoaded, filterOptionsLoadError] =

clients/ui/frontend/src/app/modelCatalogTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export type ModelCatalogFilterStates = {
224224
[ModelCatalogStringFilterKey.LICENSE]: ModelCatalogLicense[];
225225
[ModelCatalogStringFilterKey.LANGUAGE]: AllLanguageCode[];
226226
[ModelCatalogStringFilterKey.HARDWARE_TYPE]: string[];
227-
[ModelCatalogStringFilterKey.USE_CASE]: UseCaseOptionValue | undefined;
227+
[ModelCatalogStringFilterKey.USE_CASE]: UseCaseOptionValue[];
228228
} & {
229229
[key in ModelCatalogNumberFilterKey]: number | undefined;
230230
} & {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const HardwareConfigurationTableRow: React.FC<HardwareConfigurationTableRowProps
2626
switch (field) {
2727
case 'hardware_type':
2828
return getHardwareConfiguration(performanceArtifact);
29+
case 'use_case':
30+
return getWorkloadType(performanceArtifact);
2931
case 'hardware_count':
3032
return getIntValue(customProperties, 'hardware_count');
3133
case 'requests_per_second':
@@ -52,8 +54,6 @@ const HardwareConfigurationTableRow: React.FC<HardwareConfigurationTableRowProps
5254
return formatTokenValue(getDoubleValue(customProperties, field));
5355
case 'framework_version':
5456
return getStringValue(customProperties, field);
55-
case 'use_case':
56-
return getWorkloadType(performanceArtifact);
5757
default:
5858
return '-';
5959
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,14 @@ import { useCatalogStringFilterState } from '~/app/pages/modelCatalog/utils/mode
99

1010
const MAX_VISIBLE_FILTERS = 5;
1111

12-
type ArrayFilterKey = Exclude<ModelCatalogStringFilterKey, ModelCatalogStringFilterKey.USE_CASE>;
13-
14-
type ModelCatalogStringFilterProps<K extends ArrayFilterKey> = {
12+
type ModelCatalogStringFilterProps<K extends ModelCatalogStringFilterKey> = {
1513
title: string;
1614
filterKey: K;
1715
filterToNameMapping: Partial<Record<ModelCatalogStringFilterValueType[K], string>>;
1816
filters: ModelCatalogStringFilterOptions[K];
1917
};
2018

21-
const ModelCatalogStringFilter = <K extends ArrayFilterKey>({
19+
const ModelCatalogStringFilter = <K extends ModelCatalogStringFilterKey>({
2220
title,
2321
filterKey,
2422
filterToNameMapping,
Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,38 @@
11
import * as React from 'react';
22
import {
3+
Badge,
4+
Checkbox,
35
Dropdown,
4-
DropdownItem,
5-
DropdownList,
6+
Flex,
7+
FlexItem,
68
MenuToggle,
79
MenuToggleElement,
10+
Panel,
11+
PanelMain,
812
} from '@patternfly/react-core';
9-
import { asEnumMember } from 'mod-arch-core';
1013
import { ModelCatalogStringFilterKey, UseCaseOptionValue } from '~/concepts/modelCatalog/const';
1114
import { USE_CASE_OPTIONS } from '~/app/pages/modelCatalog/utils/workloadTypeUtils';
1215
import { ModelCatalogContext } from '~/app/context/modelCatalog/ModelCatalogContext';
1316

14-
const UseCaseFilter: React.FC = () => {
17+
const WorkloadTypeFilter: React.FC = () => {
1518
const { filterData, setFilterData } = React.useContext(ModelCatalogContext);
16-
const selectedUseCase = filterData[ModelCatalogStringFilterKey.USE_CASE];
19+
const selectedUseCases = filterData[ModelCatalogStringFilterKey.USE_CASE];
1720
const [isOpen, setIsOpen] = React.useState(false);
1821

19-
const handleUseCaseChange = (useCase: string) => {
20-
const useCaseValue = asEnumMember(useCase, UseCaseOptionValue);
21-
if (useCaseValue) {
22-
const newValue = useCaseValue === selectedUseCase ? undefined : useCaseValue;
23-
setFilterData(ModelCatalogStringFilterKey.USE_CASE, newValue);
24-
}
25-
setIsOpen(false);
26-
};
22+
const selectedCount = selectedUseCases.length;
23+
24+
const isUseCaseSelected = (value: UseCaseOptionValue): boolean =>
25+
selectedUseCases.includes(value);
2726

28-
// Get the display text for the toggle
29-
const getToggleText = () => {
30-
if (selectedUseCase) {
31-
const selectedOption = USE_CASE_OPTIONS.find((option) => option.value === selectedUseCase);
32-
return selectedOption ? (
33-
<>
34-
<strong>Workload type:</strong> {selectedOption.label}
35-
</>
36-
) : (
37-
'Workload type'
27+
const toggleUseCaseSelection = (value: UseCaseOptionValue, selected: boolean) => {
28+
if (selected) {
29+
setFilterData(ModelCatalogStringFilterKey.USE_CASE, [...selectedUseCases, value]);
30+
} else {
31+
setFilterData(
32+
ModelCatalogStringFilterKey.USE_CASE,
33+
selectedUseCases.filter((item) => item !== value),
3834
);
3935
}
40-
return 'Workload type';
4136
};
4237

4338
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
@@ -47,31 +42,51 @@ const UseCaseFilter: React.FC = () => {
4742
onClick={() => setIsOpen(!isOpen)}
4843
isExpanded={isOpen}
4944
style={{ minWidth: '200px', width: 'fit-content' }}
45+
badge={selectedCount > 0 ? <Badge>{selectedCount} selected</Badge> : undefined}
5046
>
51-
{getToggleText()}
47+
Workload type
5248
</MenuToggle>
5349
);
5450

51+
const filterContent = (
52+
<Panel>
53+
<PanelMain className="pf-v6-u-p-md">
54+
<Flex direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsSm' }}>
55+
{/* Workload type checkboxes */}
56+
<FlexItem>
57+
<Flex direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsXs' }}>
58+
{USE_CASE_OPTIONS.map((option) => (
59+
<FlexItem key={option.value}>
60+
<Flex alignItems={{ default: 'alignItemsCenter' }}>
61+
<FlexItem flex={{ default: 'flex_1' }}>
62+
<Checkbox
63+
label={option.label}
64+
id={option.value}
65+
data-testid={`workload-type-filter-${option.value}`}
66+
isChecked={isUseCaseSelected(option.value)}
67+
onChange={(_, checked) => toggleUseCaseSelection(option.value, checked)}
68+
/>
69+
</FlexItem>
70+
</Flex>
71+
</FlexItem>
72+
))}
73+
</Flex>
74+
</FlexItem>
75+
</Flex>
76+
</PanelMain>
77+
</Panel>
78+
);
79+
5580
return (
5681
<Dropdown
5782
isOpen={isOpen}
5883
onOpenChange={setIsOpen}
5984
toggle={toggle}
6085
shouldFocusToggleOnSelect={false}
6186
>
62-
<DropdownList>
63-
{USE_CASE_OPTIONS.map((option) => (
64-
<DropdownItem
65-
key={option.value}
66-
onClick={() => handleUseCaseChange(option.value)}
67-
isSelected={selectedUseCase === option.value}
68-
>
69-
{option.label}
70-
</DropdownItem>
71-
))}
72-
</DropdownList>
87+
{filterContent}
7388
</Dropdown>
7489
);
7590
};
7691

77-
export default UseCaseFilter;
92+
export default WorkloadTypeFilter;

0 commit comments

Comments
 (0)