diff --git a/src/Components/Blueprints/BlueprintsSideBar.tsx b/src/Components/Blueprints/BlueprintsSideBar.tsx index ec5f562618..c160eee6d7 100644 --- a/src/Components/Blueprints/BlueprintsSideBar.tsx +++ b/src/Components/Blueprints/BlueprintsSideBar.tsx @@ -1,6 +1,7 @@ -import React, { useState, Dispatch, SetStateAction } from 'react'; +import React, { useState, useCallback } from 'react'; import { + Bullseye, Button, EmptyState, EmptyStateActions, @@ -9,85 +10,174 @@ import { EmptyStateHeader, EmptyStateIcon, SearchInput, + Spinner, Stack, StackItem, } from '@patternfly/react-core'; -import { PlusCircleIcon } from '@patternfly/react-icons'; +import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons'; +import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon'; +import debounce from 'lodash/debounce'; import BlueprintCard from './BlueprintCard'; -import { BlueprintItem } from '../../store/imageBuilderApi'; +import { + useGetBlueprintsQuery, + BlueprintItem, +} from '../../store/imageBuilderApi'; type blueprintProps = { - blueprints: BlueprintItem[] | undefined; selectedBlueprint: string | undefined; - setSelectedBlueprint: Dispatch>; + setSelectedBlueprint: React.Dispatch< + React.SetStateAction + >; +}; + +type blueprintSearchProps = { + filter: string | undefined; + setFilter: React.Dispatch>; + blueprintsTotal: number; +}; + +type emptyBlueprintStateProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: React.ComponentClass; + action: React.ReactNode; + titleText: string; + bodyText: string; }; const BlueprintsSidebar = ({ - blueprints, selectedBlueprint, setSelectedBlueprint, }: blueprintProps) => { - const [blueprintFilter, setBlueprintFilter] = useState(''); + const [blueprintsSearchQuery, setBlueprintsSearchQuery] = useState< + string | undefined + >(); + const debouncedSearch = useCallback( + debounce((filter) => { + setBlueprintsSearchQuery(filter.length > 0 ? filter : undefined); + }, 300), + [setBlueprintsSearchQuery] + ); + React.useEffect(() => { + return () => { + debouncedSearch.cancel(); + }; + }, [debouncedSearch]); + const { data: blueprintsData, isLoading } = useGetBlueprintsQuery({ + search: blueprintsSearchQuery, + }); + const blueprints = blueprintsData?.data; - const onChange = (value: string) => { - setBlueprintFilter(value); - }; + const blueprintsTotal = blueprintsData?.meta?.count || 0; + + if (isLoading) { + return ( + + + + ); + } - const emptyBlueprints = ( - - Create} titleText="No blueprints yet" - headingLevel="h4" - icon={} + bodyText="To get started, create a blueprint." /> - To get started, create a blueprint. - - - - - - - ); - - if (blueprints === undefined || blueprints?.length === 0) { - return emptyBlueprints; + ); } return ( <> - - onChange(value)} - onClear={() => onChange('')} + {(blueprintsTotal > 0 || blueprintsSearchQuery !== undefined) && ( + <> + + + + + + + + )} + {blueprintsTotal === 0 && ( + debouncedSearch('')}> + Clear all filters + + } + titleText="No blueprints found" + bodyText="No blueprints match your search criteria. Try a different search." /> - - - - - {blueprints.map((blueprint: BlueprintItem) => ( - - - - ))} + )} + {blueprintsTotal > 0 && + blueprints?.map((blueprint: BlueprintItem) => ( + + + + ))} ); }; +const BlueprintSearch = ({ + filter, + setFilter, + blueprintsTotal, +}: blueprintSearchProps) => { + const onChange = (value: string) => { + setFilter(value); + }; + + return ( + onChange(value)} + onClear={() => onChange('')} + resultsCount={`${blueprintsTotal} blueprints`} + /> + ); +}; + +const EmptyBlueprintState = ({ + titleText, + bodyText, + icon, + action, +}: emptyBlueprintStateProps) => ( + + } + /> + {bodyText} + + {action} + + +); + export default BlueprintsSidebar; diff --git a/src/Components/LandingPage/LandingPage.tsx b/src/Components/LandingPage/LandingPage.tsx index 5cc473224a..c176cf85b7 100644 --- a/src/Components/LandingPage/LandingPage.tsx +++ b/src/Components/LandingPage/LandingPage.tsx @@ -11,11 +11,9 @@ import { TextContent, TabAction, PageSection, - Spinner, Sidebar, SidebarContent, SidebarPanel, - Bullseye, } from '@patternfly/react-core'; import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons'; import { useFlag } from '@unleash/proxy-client-react'; @@ -25,7 +23,6 @@ import './LandingPage.scss'; import Quickstarts from './Quickstarts'; -import { useGetBlueprintsQuery } from '../../store/imageBuilderApi'; import { manageEdgeImagesUrlName } from '../../Utilities/edge'; import { resolveRelPath } from '../../Utilities/path'; import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar'; @@ -56,7 +53,6 @@ export const LandingPage = () => { const [selectedBlueprint, setSelectedBlueprint] = useState< string | undefined >(); - const { data: blueprints, isLoading } = useGetBlueprintsQuery({}); const edgeParityFlag = useFlag('edgeParity.image-list'); const experimentalFlag = @@ -82,7 +78,6 @@ export const LandingPage = () => { @@ -99,14 +94,6 @@ export const LandingPage = () => { ? experimentalImageList : traditionalImageList; - if (isLoading) { - return ( - - - - ); - } - return ( <> diff --git a/src/test/Components/Blueprints/Blueprints.test.js b/src/test/Components/Blueprints/Blueprints.test.js index 6e2452e81b..d1dc3354f2 100644 --- a/src/test/Components/Blueprints/Blueprints.test.js +++ b/src/test/Components/Blueprints/Blueprints.test.js @@ -1,4 +1,4 @@ -import { screen, within } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { rest } from 'msw'; @@ -71,4 +71,21 @@ describe('Blueprints', () => { await user.click(blueprintRadioBtn); expect(screen.queryByTestId('images-table')).not.toBeInTheDocument(); }); + + describe('filtering', () => { + test('filter blueprints', async () => { + renderWithReduxRouter('', {}); + + const searchInput = await screen.findByPlaceholderText( + 'Search by name or description' + ); + searchInput.focus(); + await user.keyboard('Milk'); + + // wait for debounce + await waitFor(() => { + expect(screen.getAllByRole('radio')).toHaveLength(1); + }); + }); + }); }); diff --git a/src/test/mocks/handlers.js b/src/test/mocks/handlers.js index 6f2312319b..eddeba792c 100644 --- a/src/test/mocks/handlers.js +++ b/src/test/mocks/handlers.js @@ -113,7 +113,17 @@ export const handlers = [ } ), rest.get(`${IMAGE_BUILDER_API}/experimental/blueprints`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(mockGetBlueprints)); + const search = req.url.searchParams.get('search'); + const resp = mockGetBlueprints; + if (search) { + const regexp = new RegExp(search + '*'); + resp.data = mockGetBlueprints.data.filter(({ name }) => + regexp.test(name) + ); + resp.meta.count = resp.data.length; + } + + return res(ctx.status(200), ctx.json(resp)); }), rest.get( `${IMAGE_BUILDER_API}/experimental/blueprints/:id/composes`,