Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-marketplace': patch
---

Fix Support Type filter count inconsistency when other filters are applied.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ jest.mock('../hooks/usePluginFacets', () => ({
}),
}));

jest.mock('../hooks/useFilteredPluginFacet', () => ({
useFilteredPluginFacet: jest.fn().mockReturnValue({
data: [],
}),
}));

jest.mock('../hooks/useFilteredSupportTypes', () => ({
useFilteredSupportTypes: jest.fn().mockReturnValue({
data: [],
}),
}));

afterAll(() => {
jest.clearAllMocks();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,18 @@ import { useSearchParams } from 'react-router-dom';

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

import {
MarketplaceAnnotation,
MarketplaceSupportLevel,
} from '@red-hat-developer-hub/backstage-plugin-marketplace-common';

import { usePluginFacet } from '../hooks/usePluginFacet';
import { usePluginFacets } from '../hooks/usePluginFacets';
import { useFilteredPluginFacet } from '../hooks/useFilteredPluginFacet';
import { useFilteredSupportTypes } from '../hooks/useFilteredSupportTypes';
import {
CustomSelectFilter,
CustomSelectItem,
} from '../shared-components/CustomSelectFilter';
import { useQueryArrayFilter } from '../hooks/useQueryArrayFilter';
import { colors } from '../consts';
import { useTranslation } from '../hooks/useTranslation';

const CategoryFilter = () => {
const { t } = useTranslation();
const categoriesFacet = usePluginFacet('spec.categories');
const categoriesFacet = useFilteredPluginFacet('spec.categories', 'category');
const filter = useQueryArrayFilter('category');
const categories = categoriesFacet.data;

Expand Down Expand Up @@ -70,7 +64,7 @@ const CategoryFilter = () => {

const AuthorFilter = () => {
const { t } = useTranslation();
const authorsFacet = usePluginFacet('spec.authors.name');
const authorsFacet = useFilteredPluginFacet('spec.authors.name', 'author');
const authors = authorsFacet.data;
const filter = useQueryArrayFilter('author');

Expand Down Expand Up @@ -101,12 +95,6 @@ const AuthorFilter = () => {
);
};

const facetsKeys = [
`metadata.annotations.${MarketplaceAnnotation.CERTIFIED_BY}`,
`metadata.annotations.${MarketplaceAnnotation.PRE_INSTALLED}`,
'spec.support.level',
];

const evaluateParams = (
newSelection: (string | number)[],
newParams: URLSearchParams,
Expand All @@ -121,100 +109,9 @@ const evaluateParams = (
const SupportTypeFilter = () => {
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const pluginFacets = usePluginFacets({ facets: facetsKeys });

const facets = pluginFacets.data;

const items = useMemo(() => {
if (!facets) return [];
const allSupportTypeItems: CustomSelectItem[] = [];

// Certified plugins
const certified = facets[facetsKeys[0]];
const certifiedCount =
certified?.reduce((acc, curr) => acc + curr.count, 0) || 0;
// const certifiedFilter = certified?.map(c => c.value).join(', ') || '';
const certifiedProviders = certified?.map(c => c.value).join(', ') || '';

allSupportTypeItems.push({
label: t('badges.certified'),
value: 'certified',
count: certifiedCount,
isBadge: true,
badgeColor: colors.certified,
helperText: t('badges.stableAndSecured' as any, {
provider: certifiedProviders,
}),
displayOrder: 2,
});

// Custom plugins
const preinstalled = facets[facetsKeys[1]];
const customCount =
preinstalled?.find(p => p.value === 'false')?.count ?? 0;
if (customCount > 0) {
allSupportTypeItems.push({
label: t('badges.customPlugin'),
value: 'custom',
count: customCount,
isBadge: true,
badgeColor: colors.custom,
helperText: t('badges.addedByAdmin'),
displayOrder: 3,
});
}

const supportLevelFilters = facets[facetsKeys[2]];
supportLevelFilters?.forEach(supportLevelFilter => {
if (
supportLevelFilter.value === MarketplaceSupportLevel.GENERALLY_AVAILABLE
) {
allSupportTypeItems.push({
label: t('badges.generallyAvailable'),
value: `support-level=${supportLevelFilter.value}`,
count: supportLevelFilter.count,
isBadge: true,
badgeColor: colors.generallyAvailable,
helperText: t('badges.productionReady'),
displayOrder: 1,
});
} else if (
supportLevelFilter.value === MarketplaceSupportLevel.TECH_PREVIEW
) {
allSupportTypeItems.push({
label: t('badges.techPreview'),
value: `support-level=${supportLevelFilter.value}`,
count: supportLevelFilter.count,
helperText: t('badges.pluginInDevelopment'),
displayOrder: 4,
});
} else if (
supportLevelFilter.value === MarketplaceSupportLevel.DEV_PREVIEW
) {
allSupportTypeItems.push({
label: t('badges.devPreview'),
value: `support-level=${supportLevelFilter.value}`,
count: supportLevelFilter.count,
helperText: t('badges.earlyStageExperimental'),
displayOrder: 5,
});
} else if (
supportLevelFilter.value === MarketplaceSupportLevel.COMMUNITY
) {
allSupportTypeItems.push({
label: t('badges.communityPlugin'),
value: `support-level=${supportLevelFilter.value}`,
count: supportLevelFilter.count,
helperText: t('badges.openSourceNoSupport'),
displayOrder: 6,
});
}
});
const filteredSupportTypes = useFilteredSupportTypes();

return allSupportTypeItems.sort(
(a, b) => (a.displayOrder || 0) - (b.displayOrder || 0),
);
}, [facets, t]);
const items = filteredSupportTypes.data;

const selected = useMemo(() => {
const selectedFilters = searchParams.getAll('filter');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';

import { MarketplaceAnnotation } from '@red-hat-developer-hub/backstage-plugin-marketplace-common';

import { useMarketplaceApi } from './useMarketplaceApi';

/**
* Hook to get plugin facets filtered by current active filters
* @param facet - The facet field to get values for
* @param excludeFilterType - The filter type to exclude from the filter (allows getting options for the current filter)
*/
export const useFilteredPluginFacet = (
facet: string,
excludeFilterType?: string,
) => {
const [searchParams] = useSearchParams();
const marketplaceApi = useMarketplaceApi();

const filters = searchParams.getAll('filter');

// Get all plugins and apply client-side filtering for accurate facet calculation
const pluginsQuery = useQuery({
queryKey: ['marketplaceApi', 'getPlugins'],
queryFn: () =>
marketplaceApi.getPlugins({
orderFields: [{ field: 'metadata.title', order: 'asc' }],
}),
});

return useQuery({
queryKey: [
'marketplaceApi',
'getFilteredPluginFacet',
facet,
filters,
excludeFilterType,
],
queryFn: async () => {
if (!pluginsQuery.data?.items) return undefined;

// Apply filtering excluding the specified filter type
const activeFilters = filters.filter(filter => {
if (!excludeFilterType) return true;

// Exclude filters of the specified type
if (
excludeFilterType === 'category' &&
filter.startsWith('category=')
) {
return false;
}
if (excludeFilterType === 'author' && filter.startsWith('author=')) {
return false;
}
if (
excludeFilterType === 'support' &&
(filter === 'certified' ||
filter === 'custom' ||
filter.startsWith('support-level='))
) {
return false;
}
return true;
});

let filteredPlugins = pluginsQuery.data.items;

// Apply category filters
const categories = activeFilters
.filter(filter => filter.startsWith('category='))
.map(filter => filter.substring('category='.length));
if (categories.length > 0) {
filteredPlugins = filteredPlugins.filter(plugin =>
plugin.spec?.categories?.some(category =>
categories.includes(category),
),
);
}

// Apply author filters
const authors = activeFilters
.filter(filter => filter.startsWith('author='))
.map(filter => filter.substring('author='.length));
if (authors.length > 0) {
filteredPlugins = filteredPlugins.filter(plugin => {
// Check spec.authors array
if (
plugin.spec?.authors?.some(author =>
typeof author === 'string'
? authors.includes(author)
: authors.includes(author.name),
)
) {
return true;
}
// Check certification annotation as fallback
const certifiedBy =
plugin.metadata?.annotations?.[MarketplaceAnnotation.CERTIFIED_BY];
return certifiedBy && authors.includes(certifiedBy);
});
}

// Apply support type filters
const showCertified = activeFilters.includes('certified');
const showCustom = activeFilters.includes('custom');
const supportLevels = activeFilters
.filter(filter => filter.startsWith('support-level='))
.map(filter => filter.substring('support-level='.length));

if (showCertified || showCustom || supportLevels.length > 0) {
filteredPlugins = filteredPlugins.filter(plugin => {
if (
showCertified &&
plugin.metadata?.annotations?.[MarketplaceAnnotation.CERTIFIED_BY]
) {
return true;
}
if (
showCustom &&
plugin.metadata?.annotations?.[
MarketplaceAnnotation.PRE_INSTALLED
] !== 'true'
) {
return true;
}
if (supportLevels.length > 0 && plugin.spec?.support?.level) {
return supportLevels.includes(plugin.spec.support.level);
}
return false;
});
}

// Calculate facet values from filtered plugins
const facetValues: Record<string, number> = {};

filteredPlugins.forEach(plugin => {
let values: any[] = [];

// Extract values based on facet path
if (facet === 'spec.categories') {
values = plugin.spec?.categories || [];
} else if (facet === 'spec.authors.name') {
if (plugin.spec?.authors && plugin.spec.authors.length > 0) {
values = plugin.spec.authors
.map(author =>
typeof author === 'string' ? author : author.name,
)
.filter(Boolean);
} else if (plugin.spec?.author) {
values = [plugin.spec.author];
}
}

values.forEach(value => {
facetValues[value] = (facetValues[value] || 0) + 1;
});
});

// Convert to expected format
const result = Object.entries(facetValues).map(([value, count]) => ({
value,
count,
}));
return result;
},
enabled: !!pluginsQuery.data,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,22 @@ export const useFilteredPlugins = () => {
.filter(filter => filter.startsWith('author='))
.map(filter => filter.substring('author='.length));
if (authors.length > 0) {
plugins = plugins.filter(plugin =>
plugin.spec?.authors?.some(author =>
typeof author === 'string'
? authors.includes(author)
: authors.includes(author.name),
),
);
plugins = plugins.filter(plugin => {
if (
plugin.spec?.authors?.some(author =>
typeof author === 'string'
? authors.includes(author)
: authors.includes(author.name),
)
) {
return true;
}

if (plugin.spec?.author && authors.includes(plugin.spec.author)) {
return true;
}
return false;
});
}

const showCertified = filters.includes('certified');
Expand Down
Loading