Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance filterTiles with scoring, filtering, and sorting #1547

Merged
merged 2 commits into from
Oct 10, 2024
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
27 changes: 14 additions & 13 deletions packages/ui/src/components/Catalog/Catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,27 @@
/** Selected Providers */
const [selectedProviders, setSelectedProviders] = useState<string[]>(providers);

/** Filter by selected group */
const filteredTilesByGroup = useMemo(() => {
const filteredTiles = useMemo(() => {

Check warning on line 45 in packages/ui/src/components/Catalog/Catalog.tsx

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/Catalog.tsx#L45

Added line #L45 was not covered by tests
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<string, ITile[]> = {};
filteredTiles.forEach((tile) => {

Check warning on line 52 in packages/ui/src/components/Catalog/Catalog.tsx

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/Catalog.tsx#L51-L52

Added lines #L51 - L52 were not covered by tests
if (!groups[tile.type]) {
groups[tile.type] = [];

Check warning on line 54 in packages/ui/src/components/Catalog/Catalog.tsx

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/Catalog.tsx#L54

Added line #L54 was not covered by tests
}
groups[tile.type].push(tile);

Check warning on line 56 in packages/ui/src/components/Catalog/Catalog.tsx

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/Catalog.tsx#L56

Added line #L56 was not covered by tests
});
return Object.entries(groups).map(([group, tiles]) => ({ name: group, count: tiles.length }));

Check warning on line 58 in packages/ui/src/components/Catalog/Catalog.tsx

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/Catalog.tsx#L58

Added line #L58 was not covered by tests
}, [filteredTiles]);

const [activeGroups, setActiveGroups] = useState<string[]>(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<ITile[]>(() => {
return filteredTiles.filter((tile) => activeGroups.includes(tile.type));

Check warning on line 64 in packages/ui/src/components/Catalog/Catalog.tsx

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/Catalog.tsx#L63-L64

Added lines #L63 - L64 were not covered by tests
}, [activeGroups, filteredTiles]);

const onFilterChange = useCallback(
(_event: unknown, value = '') => {
Expand Down Expand Up @@ -116,7 +117,7 @@
/>
<BaseCatalog
className="catalog__base"
tiles={filteredTiles}
tiles={filteredTilesByGroup}
catalogLayout={activeLayout}
onTileClick={onTileClick}
onTagClick={onTagClick}
Expand Down
44 changes: 14 additions & 30 deletions packages/ui/src/components/Catalog/filter-tiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,62 +69,46 @@ describe('filterTiles', () => {
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]);
});
});
84 changes: 50 additions & 34 deletions packages/ui/src/components/Catalog/filter-tiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,57 @@
export const filterTiles = (
tiles: ITile[],
options?: { searchTerm?: string; searchTags?: string[]; selectedProviders?: string[] },
): Record<string, ITile[]> => {
): ITile[] => {
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<ITile['type'], ITile[]>,
);
// 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;

Check warning on line 22 in packages/ui/src/components/Catalog/filter-tiles.ts

View check run for this annotation

Codecov / codecov/patch

packages/ui/src/components/Catalog/filter-tiles.ts#L22

Added line #L22 was not covered by tests
}

// 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
const tilesResult: ITile[] = filteredTiles.sort((a, b) => b.score - a.score).map(({ tile }) => tile);

return tilesResult;
};
Loading