Skip to content

Commit 2a445d2

Browse files
Extension: Fix Support Type filter count inconsistency when other filters are applied (#1546)
Co-authored-by: Mitesh Kumar <itsmiteshkumar98@gmail.com>
1 parent eee53b9 commit 2a445d2

6 files changed

Lines changed: 438 additions & 116 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-marketplace': patch
3+
---
4+
5+
Fix Support Type filter count inconsistency when other filters are applied.

workspaces/marketplace/plugins/marketplace/src/components/MarketplaceCatalogContent.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ jest.mock('../hooks/usePluginFacets', () => ({
5252
}),
5353
}));
5454

55+
jest.mock('../hooks/useFilteredPluginFacet', () => ({
56+
useFilteredPluginFacet: jest.fn().mockReturnValue({
57+
data: [],
58+
}),
59+
}));
60+
61+
jest.mock('../hooks/useFilteredSupportTypes', () => ({
62+
useFilteredSupportTypes: jest.fn().mockReturnValue({
63+
data: [],
64+
}),
65+
}));
66+
5567
afterAll(() => {
5668
jest.clearAllMocks();
5769
});

workspaces/marketplace/plugins/marketplace/src/components/MarketplacePluginFilter.tsx

Lines changed: 6 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,18 @@ import { useSearchParams } from 'react-router-dom';
2020

2121
import Box from '@mui/material/Box';
2222

23-
import {
24-
MarketplaceAnnotation,
25-
MarketplaceSupportLevel,
26-
} from '@red-hat-developer-hub/backstage-plugin-marketplace-common';
27-
28-
import { usePluginFacet } from '../hooks/usePluginFacet';
29-
import { usePluginFacets } from '../hooks/usePluginFacets';
23+
import { useFilteredPluginFacet } from '../hooks/useFilteredPluginFacet';
24+
import { useFilteredSupportTypes } from '../hooks/useFilteredSupportTypes';
3025
import {
3126
CustomSelectFilter,
3227
CustomSelectItem,
3328
} from '../shared-components/CustomSelectFilter';
3429
import { useQueryArrayFilter } from '../hooks/useQueryArrayFilter';
35-
import { colors } from '../consts';
3630
import { useTranslation } from '../hooks/useTranslation';
3731

3832
const CategoryFilter = () => {
3933
const { t } = useTranslation();
40-
const categoriesFacet = usePluginFacet('spec.categories');
34+
const categoriesFacet = useFilteredPluginFacet('spec.categories', 'category');
4135
const filter = useQueryArrayFilter('category');
4236
const categories = categoriesFacet.data;
4337

@@ -70,7 +64,7 @@ const CategoryFilter = () => {
7064

7165
const AuthorFilter = () => {
7266
const { t } = useTranslation();
73-
const authorsFacet = usePluginFacet('spec.authors.name');
67+
const authorsFacet = useFilteredPluginFacet('spec.authors.name', 'author');
7468
const authors = authorsFacet.data;
7569
const filter = useQueryArrayFilter('author');
7670

@@ -101,12 +95,6 @@ const AuthorFilter = () => {
10195
);
10296
};
10397

104-
const facetsKeys = [
105-
`metadata.annotations.${MarketplaceAnnotation.CERTIFIED_BY}`,
106-
`metadata.annotations.${MarketplaceAnnotation.PRE_INSTALLED}`,
107-
'spec.support.level',
108-
];
109-
11098
const evaluateParams = (
11199
newSelection: (string | number)[],
112100
newParams: URLSearchParams,
@@ -121,100 +109,9 @@ const evaluateParams = (
121109
const SupportTypeFilter = () => {
122110
const { t } = useTranslation();
123111
const [searchParams, setSearchParams] = useSearchParams();
124-
const pluginFacets = usePluginFacets({ facets: facetsKeys });
125-
126-
const facets = pluginFacets.data;
127-
128-
const items = useMemo(() => {
129-
if (!facets) return [];
130-
const allSupportTypeItems: CustomSelectItem[] = [];
131-
132-
// Certified plugins
133-
const certified = facets[facetsKeys[0]];
134-
const certifiedCount =
135-
certified?.reduce((acc, curr) => acc + curr.count, 0) || 0;
136-
// const certifiedFilter = certified?.map(c => c.value).join(', ') || '';
137-
const certifiedProviders = certified?.map(c => c.value).join(', ') || '';
138-
139-
allSupportTypeItems.push({
140-
label: t('badges.certified'),
141-
value: 'certified',
142-
count: certifiedCount,
143-
isBadge: true,
144-
badgeColor: colors.certified,
145-
helperText: t('badges.stableAndSecured' as any, {
146-
provider: certifiedProviders,
147-
}),
148-
displayOrder: 2,
149-
});
150-
151-
// Custom plugins
152-
const preinstalled = facets[facetsKeys[1]];
153-
const customCount =
154-
preinstalled?.find(p => p.value === 'false')?.count ?? 0;
155-
if (customCount > 0) {
156-
allSupportTypeItems.push({
157-
label: t('badges.customPlugin'),
158-
value: 'custom',
159-
count: customCount,
160-
isBadge: true,
161-
badgeColor: colors.custom,
162-
helperText: t('badges.addedByAdmin'),
163-
displayOrder: 3,
164-
});
165-
}
166-
167-
const supportLevelFilters = facets[facetsKeys[2]];
168-
supportLevelFilters?.forEach(supportLevelFilter => {
169-
if (
170-
supportLevelFilter.value === MarketplaceSupportLevel.GENERALLY_AVAILABLE
171-
) {
172-
allSupportTypeItems.push({
173-
label: t('badges.generallyAvailable'),
174-
value: `support-level=${supportLevelFilter.value}`,
175-
count: supportLevelFilter.count,
176-
isBadge: true,
177-
badgeColor: colors.generallyAvailable,
178-
helperText: t('badges.productionReady'),
179-
displayOrder: 1,
180-
});
181-
} else if (
182-
supportLevelFilter.value === MarketplaceSupportLevel.TECH_PREVIEW
183-
) {
184-
allSupportTypeItems.push({
185-
label: t('badges.techPreview'),
186-
value: `support-level=${supportLevelFilter.value}`,
187-
count: supportLevelFilter.count,
188-
helperText: t('badges.pluginInDevelopment'),
189-
displayOrder: 4,
190-
});
191-
} else if (
192-
supportLevelFilter.value === MarketplaceSupportLevel.DEV_PREVIEW
193-
) {
194-
allSupportTypeItems.push({
195-
label: t('badges.devPreview'),
196-
value: `support-level=${supportLevelFilter.value}`,
197-
count: supportLevelFilter.count,
198-
helperText: t('badges.earlyStageExperimental'),
199-
displayOrder: 5,
200-
});
201-
} else if (
202-
supportLevelFilter.value === MarketplaceSupportLevel.COMMUNITY
203-
) {
204-
allSupportTypeItems.push({
205-
label: t('badges.communityPlugin'),
206-
value: `support-level=${supportLevelFilter.value}`,
207-
count: supportLevelFilter.count,
208-
helperText: t('badges.openSourceNoSupport'),
209-
displayOrder: 6,
210-
});
211-
}
212-
});
112+
const filteredSupportTypes = useFilteredSupportTypes();
213113

214-
return allSupportTypeItems.sort(
215-
(a, b) => (a.displayOrder || 0) - (b.displayOrder || 0),
216-
);
217-
}, [facets, t]);
114+
const items = filteredSupportTypes.data;
218115

219116
const selected = useMemo(() => {
220117
const selectedFilters = searchParams.getAll('filter');
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useQuery } from '@tanstack/react-query';
18+
import { useSearchParams } from 'react-router-dom';
19+
20+
import { MarketplaceAnnotation } from '@red-hat-developer-hub/backstage-plugin-marketplace-common';
21+
22+
import { useMarketplaceApi } from './useMarketplaceApi';
23+
24+
/**
25+
* Hook to get plugin facets filtered by current active filters
26+
* @param facet - The facet field to get values for
27+
* @param excludeFilterType - The filter type to exclude from the filter (allows getting options for the current filter)
28+
*/
29+
export const useFilteredPluginFacet = (
30+
facet: string,
31+
excludeFilterType?: string,
32+
) => {
33+
const [searchParams] = useSearchParams();
34+
const marketplaceApi = useMarketplaceApi();
35+
36+
const filters = searchParams.getAll('filter');
37+
38+
// Get all plugins and apply client-side filtering for accurate facet calculation
39+
const pluginsQuery = useQuery({
40+
queryKey: ['marketplaceApi', 'getPlugins'],
41+
queryFn: () =>
42+
marketplaceApi.getPlugins({
43+
orderFields: [{ field: 'metadata.title', order: 'asc' }],
44+
}),
45+
});
46+
47+
return useQuery({
48+
queryKey: [
49+
'marketplaceApi',
50+
'getFilteredPluginFacet',
51+
facet,
52+
filters,
53+
excludeFilterType,
54+
],
55+
queryFn: async () => {
56+
if (!pluginsQuery.data?.items) return undefined;
57+
58+
// Apply filtering excluding the specified filter type
59+
const activeFilters = filters.filter(filter => {
60+
if (!excludeFilterType) return true;
61+
62+
// Exclude filters of the specified type
63+
if (
64+
excludeFilterType === 'category' &&
65+
filter.startsWith('category=')
66+
) {
67+
return false;
68+
}
69+
if (excludeFilterType === 'author' && filter.startsWith('author=')) {
70+
return false;
71+
}
72+
if (
73+
excludeFilterType === 'support' &&
74+
(filter === 'certified' ||
75+
filter === 'custom' ||
76+
filter.startsWith('support-level='))
77+
) {
78+
return false;
79+
}
80+
return true;
81+
});
82+
83+
let filteredPlugins = pluginsQuery.data.items;
84+
85+
// Apply category filters
86+
const categories = activeFilters
87+
.filter(filter => filter.startsWith('category='))
88+
.map(filter => filter.substring('category='.length));
89+
if (categories.length > 0) {
90+
filteredPlugins = filteredPlugins.filter(plugin =>
91+
plugin.spec?.categories?.some(category =>
92+
categories.includes(category),
93+
),
94+
);
95+
}
96+
97+
// Apply author filters
98+
const authors = activeFilters
99+
.filter(filter => filter.startsWith('author='))
100+
.map(filter => filter.substring('author='.length));
101+
if (authors.length > 0) {
102+
filteredPlugins = filteredPlugins.filter(plugin => {
103+
// Check spec.authors array
104+
if (
105+
plugin.spec?.authors?.some(author =>
106+
typeof author === 'string'
107+
? authors.includes(author)
108+
: authors.includes(author.name),
109+
)
110+
) {
111+
return true;
112+
}
113+
// Check certification annotation as fallback
114+
const certifiedBy =
115+
plugin.metadata?.annotations?.[MarketplaceAnnotation.CERTIFIED_BY];
116+
return certifiedBy && authors.includes(certifiedBy);
117+
});
118+
}
119+
120+
// Apply support type filters
121+
const showCertified = activeFilters.includes('certified');
122+
const showCustom = activeFilters.includes('custom');
123+
const supportLevels = activeFilters
124+
.filter(filter => filter.startsWith('support-level='))
125+
.map(filter => filter.substring('support-level='.length));
126+
127+
if (showCertified || showCustom || supportLevels.length > 0) {
128+
filteredPlugins = filteredPlugins.filter(plugin => {
129+
if (
130+
showCertified &&
131+
plugin.metadata?.annotations?.[MarketplaceAnnotation.CERTIFIED_BY]
132+
) {
133+
return true;
134+
}
135+
if (
136+
showCustom &&
137+
plugin.metadata?.annotations?.[
138+
MarketplaceAnnotation.PRE_INSTALLED
139+
] !== 'true'
140+
) {
141+
return true;
142+
}
143+
if (supportLevels.length > 0 && plugin.spec?.support?.level) {
144+
return supportLevels.includes(plugin.spec.support.level);
145+
}
146+
return false;
147+
});
148+
}
149+
150+
// Calculate facet values from filtered plugins
151+
const facetValues: Record<string, number> = {};
152+
153+
filteredPlugins.forEach(plugin => {
154+
let values: any[] = [];
155+
156+
// Extract values based on facet path
157+
if (facet === 'spec.categories') {
158+
values = plugin.spec?.categories || [];
159+
} else if (facet === 'spec.authors.name') {
160+
if (plugin.spec?.authors && plugin.spec.authors.length > 0) {
161+
values = plugin.spec.authors
162+
.map(author =>
163+
typeof author === 'string' ? author : author.name,
164+
)
165+
.filter(Boolean);
166+
} else if (plugin.spec?.author) {
167+
values = [plugin.spec.author];
168+
}
169+
}
170+
171+
values.forEach(value => {
172+
facetValues[value] = (facetValues[value] || 0) + 1;
173+
});
174+
});
175+
176+
// Convert to expected format
177+
const result = Object.entries(facetValues).map(([value, count]) => ({
178+
value,
179+
count,
180+
}));
181+
return result;
182+
},
183+
enabled: !!pluginsQuery.data,
184+
});
185+
};

workspaces/marketplace/plugins/marketplace/src/hooks/useFilteredPlugins.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,22 @@ export const useFilteredPlugins = () => {
6767
.filter(filter => filter.startsWith('author='))
6868
.map(filter => filter.substring('author='.length));
6969
if (authors.length > 0) {
70-
plugins = plugins.filter(plugin =>
71-
plugin.spec?.authors?.some(author =>
72-
typeof author === 'string'
73-
? authors.includes(author)
74-
: authors.includes(author.name),
75-
),
76-
);
70+
plugins = plugins.filter(plugin => {
71+
if (
72+
plugin.spec?.authors?.some(author =>
73+
typeof author === 'string'
74+
? authors.includes(author)
75+
: authors.includes(author.name),
76+
)
77+
) {
78+
return true;
79+
}
80+
81+
if (plugin.spec?.author && authors.includes(plugin.spec.author)) {
82+
return true;
83+
}
84+
return false;
85+
});
7786
}
7887

7988
const showCertified = filters.includes('certified');

0 commit comments

Comments
 (0)