Skip to content

Commit

Permalink
Blueprints: add Blueprints filtering
Browse files Browse the repository at this point in the history
Enables Blueprints filtering.
Given this is a server side filtering, it also ads Debounce hook.
This hook enables delay the API request to save server roundtrips.

Refs HMS-3389
  • Loading branch information
ezr-ondrej committed Jan 30, 2024
1 parent 7f2fb34 commit de521a7
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 45 deletions.
124 changes: 83 additions & 41 deletions src/Components/Blueprints/BlueprintsSideBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, Dispatch, SetStateAction } from 'react';
import React, { Dispatch, SetStateAction } from 'react';

import {
Button,
Expand All @@ -12,71 +12,82 @@ import {
Stack,
StackItem,
} from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';

import BlueprintCard from './BlueprintCard';

import { BlueprintItem } from '../../store/imageBuilderApi';
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';

type blueprintProps = {
blueprints: BlueprintItem[] | undefined;
selectedBlueprint: string | undefined;
setSelectedBlueprint: Dispatch<SetStateAction<string | undefined>>;
filter: string | undefined;
setFilter: Dispatch<SetStateAction<string>>;
};

type blueprintSearchProps = {
filter: string | undefined;
setFilter: Dispatch<SetStateAction<string>>;
};

type emptyBlueprintStateProps = {
icon: React.ComponentClass<SVGIconProps, any>;
action: React.ReactNode;
titleText: string;
bodyText: string;
};

const BlueprintsSidebar = ({
blueprints,
selectedBlueprint,
setSelectedBlueprint,
filter,
setFilter,
}: blueprintProps) => {
const [blueprintFilter, setBlueprintFilter] = useState('');
const hasAnyBlueprints = (blueprints?.length || 0) > 0;

const onChange = (value: string) => {
setBlueprintFilter(value);
};

const emptyBlueprints = (
<EmptyState variant="sm">
<EmptyStateHeader
if (!hasAnyBlueprints && filter === undefined) {
return (
<EmptyBlueprintState
icon={PlusCircleIcon}
action={<Button>Create</Button>}
titleText="No blueprints yet"
headingLevel="h4"
icon={<EmptyStateIcon icon={PlusCircleIcon} />}
bodyText="To get started, create a blueprint."
/>
<EmptyStateBody>To get started, create a blueprint.</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button>Create</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
);

if (blueprints === undefined || blueprints?.length === 0) {
return emptyBlueprints;
);
}

return (
<>
<Stack hasGutter>
<StackItem>
<SearchInput
placeholder="Search by name or description"
value={blueprintFilter}
onChange={(_event, value) => onChange(value)}
onClear={() => onChange('')}
{(hasAnyBlueprints || filter !== undefined) && (
<>
<StackItem>
<BlueprintSearch filter={filter} setFilter={setFilter} />
</StackItem>
<StackItem>
<Button
isBlock
onClick={() => setSelectedBlueprint(undefined)}
variant="link"
isDisabled={!selectedBlueprint}
>
Show all images
</Button>
</StackItem>
</>
)}
{!hasAnyBlueprints && (
<EmptyBlueprintState
icon={PlusCircleIcon}
action={<Button variant='link' onClick={() => setFilter('')}>Clear all filters</Button>}
titleText="No blueprints found"
bodyText="No blueprints match your search criteria. Try a different search."
/>
</StackItem>
<StackItem>
<Button
isBlock
onClick={() => setSelectedBlueprint(undefined)}
variant="link"
isDisabled={!selectedBlueprint}
>
Show all images
</Button>
</StackItem>
{blueprints.map((blueprint: BlueprintItem) => (
)}
{hasAnyBlueprints && blueprints?.map((blueprint: BlueprintItem) => (
<StackItem key={blueprint.id}>
<BlueprintCard
blueprint={blueprint}
Expand All @@ -90,4 +101,35 @@ const BlueprintsSidebar = ({
);
};

const BlueprintSearch = ({ filter, setFilter }: blueprintSearchProps) => {
const onChange = (value: string) => {
setFilter(value);
};

return (
<SearchInput
value={filter}
placeholder="Search by name or description"
onChange={(_event, value) => onChange(value)}
onClear={() => onChange('')}
/>
);
};

const EmptyBlueprintState = ({titleText, bodyText, icon, action }: emptyBlueprintStateProps) => (
<EmptyState variant="sm">
<EmptyStateHeader
titleText="No blueprints yet"
headingLevel="h4"
icon={<EmptyStateIcon icon={icon} />}
/>
<EmptyStateBody>To get started, create a blueprint.</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
{action}
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
);

export default BlueprintsSidebar;
20 changes: 18 additions & 2 deletions src/Components/LandingPage/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';

import debounce from 'lodash/debounce';
import {
Button,
Label,
Expand All @@ -25,13 +25,15 @@ import './LandingPage.scss';

import Quickstarts from './Quickstarts';

import useDebouncedSearch from '../../Hooks/useDebouncedSearch';
import { useGetBlueprintsQuery } from '../../store/imageBuilderApi';
import { manageEdgeImagesUrlName } from '../../Utilities/edge';
import { resolveRelPath } from '../../Utilities/path';
import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar';
import EdgeImagesTable from '../edge/ImagesTable';
import ImagesTable from '../ImagesTable/ImagesTable';
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
import { set } from 'lodash';

export const LandingPage = () => {
const { pathname } = useLocation();
Expand All @@ -56,7 +58,19 @@ export const LandingPage = () => {
const [selectedBlueprint, setSelectedBlueprint] = useState<
string | undefined
>();
const { data: blueprints, isLoading } = useGetBlueprintsQuery({});
const [blueprintsSearchQuery, setBlueprintsSearchQuery] = useState<string | undefined>();
const debouncedSearch = React.useCallback(
debounce((filter) => {
setBlueprintsSearchQuery(filter.length > 0 ? filter : undefined);
}, 300), [setBlueprintsSearchQuery]);
React.useEffect(() => {
return () => {
debouncedSearch.cancel();
};
}, [debouncedSearch]);
const { data: blueprints, isLoading } = useGetBlueprintsQuery({
search: blueprintsSearchQuery,
});

const edgeParityFlag = useFlag('edgeParity.image-list');
const experimentalFlag =
Expand Down Expand Up @@ -85,6 +99,8 @@ export const LandingPage = () => {
blueprints={blueprints?.data}
selectedBlueprint={selectedBlueprint}
setSelectedBlueprint={setSelectedBlueprint}
filter={blueprintsSearchQuery}
setFilter={debouncedSearch}
/>
</SidebarPanel>
<SidebarContent>
Expand Down
19 changes: 18 additions & 1 deletion src/test/Components/Blueprints/Blueprints.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
});
});
});
});
12 changes: 11 additions & 1 deletion src/test/mocks/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down

0 comments on commit de521a7

Please sign in to comment.