From ff07c618082a7e18f57a8decac282ecc702fd48c Mon Sep 17 00:00:00 2001 From: Ivo Bek Date: Wed, 9 Oct 2024 10:37:58 +0200 Subject: [PATCH 1/2] Enhance filterTiles with scoring, filtering, and sorting --- .../ui/src/components/Catalog/filter-tiles.ts | 95 ++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/packages/ui/src/components/Catalog/filter-tiles.ts b/packages/ui/src/components/Catalog/filter-tiles.ts index f7c10d641..9e850f9c2 100644 --- a/packages/ui/src/components/Catalog/filter-tiles.ts +++ b/packages/ui/src/components/Catalog/filter-tiles.ts @@ -1,3 +1,4 @@ +import { CatalogKind } from '../../models'; import { ITile } from './Catalog.models'; const checkThatArrayContainsAllTags = (tileTags: string[], searchTags: string[]) => @@ -10,37 +11,65 @@ export const filterTiles = ( const { searchTerm = '', searchTags = [], selectedProviders = [] } = options ?? {}; const searchTermLowercase = searchTerm.toLowerCase(); - return tiles.reduce( - (acc, tile) => { - /** Filter by selected tags */ - const doesTagsMatches = searchTags.length ? checkThatArrayContainsAllTags(tile.tags, searchTags) : true; - - /** Filter by providers */ - let doesProviderMatch = true; - if (selectedProviders.length) { - doesProviderMatch = - tile.provider === undefined - ? selectedProviders.includes('Community') - : selectedProviders.includes(tile.provider); - } - - /** Determine whether the tile should be included in the filtered list */ - const shouldInclude = - doesTagsMatches && - doesProviderMatch && - (!searchTermLowercase || - tile.name.toLowerCase().includes(searchTermLowercase) || - tile.title.toLowerCase().includes(searchTermLowercase) || - tile.description?.toLowerCase().includes(searchTermLowercase) || - tile.tags.some((tag) => tag.toLowerCase().includes(searchTermLowercase))); - - acc[tile.type] = acc[tile.type] ?? []; - if (shouldInclude) { - acc[tile.type].push(tile); - } - - return acc; - }, - {} as Record, - ); + // Step 1: Score each tile based on how well it matches the search term + const scoredTiles = tiles.map((tile) => { + let score = 0; + + // Score based on name + const nameLower = tile.name.toLowerCase(); + if (nameLower.startsWith(searchTermLowercase)) { + score += 100; + } else if (nameLower.includes(searchTermLowercase)) { + score += 40; + } + + // Score based on title + if (tile.title?.toLowerCase().includes(searchTermLowercase)) { + score += 40; + } + + // Score based on description + if (tile.description?.toLowerCase().includes(searchTermLowercase)) { + score += 10; + } + + return { tile, score }; + }); + + // Step 2: Filter tiles based on score, tags, and providers + const filteredTiles = scoredTiles.filter(({ tile, score }) => { + // Exclude tiles with no match + if (score <= 0) return false; + + // Filter by selected tags + const doesTagsMatch = searchTags.length ? checkThatArrayContainsAllTags(tile.tags, searchTags) : true; + + // Filter by selected providers + let doesProviderMatch = true; + if (selectedProviders.length) { + doesProviderMatch = + tile.provider === undefined + ? selectedProviders.includes('Community') + : selectedProviders.includes(tile.provider); + } + + return doesTagsMatch && doesProviderMatch; + }); + + // Step 3: Sort the filtered tiles by score in descending order + filteredTiles.sort((a, b) => b.score - a.score); + + // Step 4: Group the sorted tiles by their type + const groupedTiles: Record = {}; + groupedTiles[CatalogKind.Component] = []; + groupedTiles[CatalogKind.Kamelet] = []; + groupedTiles[CatalogKind.Pattern] = []; + filteredTiles.forEach(({ tile }) => { + if (!groupedTiles[tile.type]) { + groupedTiles[tile.type] = []; + } + groupedTiles[tile.type].push(tile); + }); + + return groupedTiles; }; From 0a5b5fd6f5549db503c6b895eb186e2a17a60f3d Mon Sep 17 00:00:00 2001 From: Ivo Bek Date: Thu, 10 Oct 2024 14:55:51 +0200 Subject: [PATCH 2/2] Fixed ordering of catalog results with multiple types --- .../ui/src/components/Catalog/Catalog.tsx | 27 ++++++------ .../components/Catalog/filter-tiles.test.ts | 44 ++++++------------- .../ui/src/components/Catalog/filter-tiles.ts | 19 ++------ 3 files changed, 31 insertions(+), 59 deletions(-) diff --git a/packages/ui/src/components/Catalog/Catalog.tsx b/packages/ui/src/components/Catalog/Catalog.tsx index 245c152eb..a132f7a0e 100644 --- a/packages/ui/src/components/Catalog/Catalog.tsx +++ b/packages/ui/src/components/Catalog/Catalog.tsx @@ -42,26 +42,27 @@ export const Catalog: FunctionComponent> = (prop /** Selected Providers */ const [selectedProviders, setSelectedProviders] = useState(providers); - /** Filter by selected group */ - const filteredTilesByGroup = useMemo(() => { + const filteredTiles = useMemo(() => { return filterTiles(props.tiles, { searchTerm, searchTags: filterTags, selectedProviders }); }, [filterTags, props.tiles, searchTerm, selectedProviders]); /** Set the tiles groups */ const tilesGroups = useMemo(() => { - return Object.entries(filteredTilesByGroup).map(([group, tiles]) => ({ name: group, count: tiles.length })); - }, [filteredTilesByGroup]); + const groups: Record = {}; + filteredTiles.forEach((tile) => { + if (!groups[tile.type]) { + groups[tile.type] = []; + } + groups[tile.type].push(tile); + }); + return Object.entries(groups).map(([group, tiles]) => ({ name: group, count: tiles.length })); + }, [filteredTiles]); const [activeGroups, setActiveGroups] = useState(tilesGroups.map((g) => g.name)); - const filteredTiles = useMemo(() => { - return Object.entries(filteredTilesByGroup).reduce((acc, [group, tiles]) => { - if (activeGroups.includes(group)) { - acc.push(...tiles); - } - return acc; - }, [] as ITile[]); - }, [activeGroups, filteredTilesByGroup]); + const filteredTilesByGroup = useMemo(() => { + return filteredTiles.filter((tile) => activeGroups.includes(tile.type)); + }, [activeGroups, filteredTiles]); const onFilterChange = useCallback( (_event: unknown, value = '') => { @@ -116,7 +117,7 @@ export const Catalog: FunctionComponent> = (prop /> { const options = { searchTerm: 'message' }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.activemq], - [CatalogKind.Pattern]: [tilesMap.setBody, tilesMap.split], - [CatalogKind.Kamelet]: [tilesMap.slackSource], - }); + expect(result).toEqual([tilesMap.activemq, tilesMap.setBody, tilesMap.split, tilesMap.slackSource]); }); it('should filter tiles by provider', () => { const options = { selectedProviders: ['Red Hat'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.cron], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.cron]); }); it('should return tiles without provider when community is selected', () => { const options = { selectedProviders: ['Community'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.activemq, tilesMap.cometd, tilesMap.hazelcast], - [CatalogKind.Pattern]: [tilesMap.setBody, tilesMap.split], - [CatalogKind.Kamelet]: [tilesMap.beerSource, tilesMap.slackSource], - }); + expect(result).toEqual([ + tilesMap.activemq, + tilesMap.cometd, + tilesMap.hazelcast, + tilesMap.setBody, + tilesMap.split, + tilesMap.beerSource, + tilesMap.slackSource, + ]); }); it('should filter tiles by a single tag', () => { const options = { searchTags: ['messaging'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.activemq, tilesMap.cometd, tilesMap.hazelcast], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.activemq, tilesMap.cometd, tilesMap.hazelcast]); }); it('should filter tiles by multiple tags', () => { const options = { searchTags: ['messaging', 'clustering'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.hazelcast], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.hazelcast]); }); it('should filter tiles by search term and tags', () => { const options = { searchTerm: 'cr', searchTags: ['scheduling'] }; const result = filterTiles(tiles, options); - expect(result).toEqual({ - [CatalogKind.Component]: [tilesMap.cron], - [CatalogKind.Pattern]: [], - [CatalogKind.Kamelet]: [], - }); + expect(result).toEqual([tilesMap.cron]); }); }); diff --git a/packages/ui/src/components/Catalog/filter-tiles.ts b/packages/ui/src/components/Catalog/filter-tiles.ts index 9e850f9c2..ee155f29f 100644 --- a/packages/ui/src/components/Catalog/filter-tiles.ts +++ b/packages/ui/src/components/Catalog/filter-tiles.ts @@ -1,4 +1,3 @@ -import { CatalogKind } from '../../models'; import { ITile } from './Catalog.models'; const checkThatArrayContainsAllTags = (tileTags: string[], searchTags: string[]) => @@ -7,7 +6,7 @@ const checkThatArrayContainsAllTags = (tileTags: string[], searchTags: string[]) export const filterTiles = ( tiles: ITile[], options?: { searchTerm?: string; searchTags?: string[]; selectedProviders?: string[] }, -): Record => { +): ITile[] => { const { searchTerm = '', searchTags = [], selectedProviders = [] } = options ?? {}; const searchTermLowercase = searchTerm.toLowerCase(); @@ -57,19 +56,7 @@ export const filterTiles = ( }); // Step 3: Sort the filtered tiles by score in descending order - filteredTiles.sort((a, b) => b.score - a.score); + const tilesResult: ITile[] = filteredTiles.sort((a, b) => b.score - a.score).map(({ tile }) => tile); - // Step 4: Group the sorted tiles by their type - const groupedTiles: Record = {}; - groupedTiles[CatalogKind.Component] = []; - groupedTiles[CatalogKind.Kamelet] = []; - groupedTiles[CatalogKind.Pattern] = []; - filteredTiles.forEach(({ tile }) => { - if (!groupedTiles[tile.type]) { - groupedTiles[tile.type] = []; - } - groupedTiles[tile.type].push(tile); - }); - - return groupedTiles; + return tilesResult; };