feat(HMS-3401): Add blueprints sidebar pagination

This commit is contained in:
Anna Vítová 2024-03-12 13:11:02 +01:00 committed by Lucas Garfield
parent 7bff1feaf4
commit a3a7ea88c5
9 changed files with 202 additions and 7 deletions

View file

@ -0,0 +1,56 @@
import React from 'react';
import {
OnSetPage,
Pagination,
PaginationVariant,
} from '@patternfly/react-core';
import {
selectBlueprintSearchInput,
selectLimit,
selectOffset,
setBlueprintLimit,
setBlueprintsOffset,
} from '../../store/BlueprintSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { useGetBlueprintsQuery } from '../../store/imageBuilderApi';
const BlueprintsPagination = () => {
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
const blueprintsOffset = useAppSelector(selectOffset) || 0;
const blueprintsLimit = useAppSelector(selectLimit) || 10;
const currPage = Math.floor(blueprintsOffset / blueprintsLimit) + 1;
const { data: blueprintsData } = useGetBlueprintsQuery({
search: blueprintSearchInput,
limit: blueprintsLimit,
offset: blueprintsOffset,
});
const dispatch = useAppDispatch();
const blueprintsTotal = blueprintsData?.meta?.count || 0;
const onSetPage: OnSetPage = (_, page) => {
const direction = page > currPage ? 1 : -1; // Calculate offset based on direction of paging
const nextOffset = blueprintsOffset + direction * blueprintsLimit;
dispatch(setBlueprintsOffset(nextOffset));
};
const onPerPageSelect: OnSetPage = (_, perPage) => {
dispatch(setBlueprintsOffset(0));
dispatch(setBlueprintLimit(perPage));
};
return (
<Pagination
variant={PaginationVariant.bottom}
itemCount={blueprintsTotal}
perPage={blueprintsLimit}
page={currPage}
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
widgetId="blueprints-pagination-bottom"
data-testid="blueprints-pagination-bottom"
isCompact
/>
);
};
export default BlueprintsPagination;

View file

@ -20,13 +20,18 @@ import debounce from 'lodash/debounce';
import { Link } from 'react-router-dom';
import BlueprintCard from './BlueprintCard';
import BlueprintsPagination from './BlueprintsPagination';
import {
selectBlueprintSearchInput,
selectLimit,
selectOffset,
selectSelectedBlueprintId,
setBlueprintId,
setBlueprintSearchInput,
setBlueprintsOffset,
} from '../../store/BlueprintSlice';
import { imageBuilderApi } from '../../store/enhancedImageBuilderApi';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import {
useGetBlueprintsQuery,
@ -49,8 +54,12 @@ type emptyBlueprintStateProps = {
const BlueprintsSidebar = () => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
const blueprintsOffset = useAppSelector(selectOffset);
const blueprintsLimit = useAppSelector(selectLimit);
const { data: blueprintsData, isLoading } = useGetBlueprintsQuery({
search: blueprintSearchInput,
limit: blueprintsLimit,
offset: blueprintsOffset,
});
const dispatch = useAppDispatch();
const blueprints = blueprintsData?.data;
@ -82,7 +91,6 @@ const BlueprintsSidebar = () => {
/>
);
}
return (
<>
<Stack hasGutter>
@ -124,6 +132,7 @@ const BlueprintsSidebar = () => {
<BlueprintCard blueprint={blueprint} />
</StackItem>
))}
<BlueprintsPagination />
</Stack>
</>
);
@ -136,6 +145,8 @@ const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => {
const dispatch = useAppDispatch();
const debouncedSearch = useCallback(
debounce((filter) => {
dispatch(setBlueprintsOffset(0));
dispatch(imageBuilderApi.util.invalidateTags([{ type: 'Blueprints' }]));
dispatch(setBlueprintSearchInput(filter.length > 0 ? filter : undefined));
}, 300),
[]

View file

@ -9,9 +9,12 @@ import {
import {
selectBlueprintSearchInput,
selectLimit,
selectOffset,
selectSelectedBlueprintId,
setBlueprintId,
} from '../../store/BlueprintSlice';
import { imageBuilderApi } from '../../store/enhancedImageBuilderApi';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import {
useDeleteBlueprintMutation,
@ -28,9 +31,15 @@ export const DeleteBlueprintModal: React.FunctionComponent<
> = ({ setShowDeleteModal, isOpen }: DeleteBlueprintModalProps) => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
const blueprintsOffset = useAppSelector(selectOffset);
const blueprintsLimit = useAppSelector(selectLimit);
const dispatch = useAppDispatch();
const { blueprintName } = useGetBlueprintsQuery(
{ search: blueprintSearchInput },
{
search: blueprintSearchInput,
limit: blueprintsLimit,
offset: blueprintsOffset,
},
{
selectFromResult: ({ data }) => ({
blueprintName: data?.data?.find(
@ -48,6 +57,7 @@ export const DeleteBlueprintModal: React.FunctionComponent<
setShowDeleteModal(false);
await deleteBlueprint({ id: selectedBlueprintId });
dispatch(setBlueprintId(undefined));
dispatch(imageBuilderApi.util.invalidateTags([{ type: 'Blueprints' }]));
}
};
const onDeleteClose = () => {

View file

@ -48,6 +48,8 @@ import {
selectBlueprintSearchInput,
selectBlueprintVersionFilter,
selectBlueprintVersionFilterAPI,
selectLimit,
selectOffset,
selectSelectedBlueprintId,
} from '../../store/BlueprintSlice';
import { useAppSelector } from '../../store/hooks';
@ -73,9 +75,15 @@ const ImagesTable = () => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
const blueprintVersionFilter = useAppSelector(selectBlueprintVersionFilter);
const blueprintsOffset = useAppSelector(selectOffset);
const blueprintsLimit = useAppSelector(selectLimit);
const { selectedBlueprintVersion } = useGetBlueprintsQuery(
{ search: blueprintSearchInput },
{
search: blueprintSearchInput,
limit: blueprintsLimit,
offset: blueprintsOffset,
},
{
selectFromResult: ({ data }) => ({
selectedBlueprintVersion: data?.data?.find(

View file

@ -9,3 +9,9 @@
.expand-section {
background-color: var(--pf-global--palette--white);
}
.sidebar-panel {
height: 750px;
overflow: auto;
}

View file

@ -74,7 +74,12 @@ export const LandingPage = () => {
</PageSection>
<PageSection className="pf-v5-u-pt-0">
<Sidebar hasBorder className="pf-v5-u-background-color-100">
<SidebarPanel hasPadding width={{ default: 'width_25' }}>
<SidebarPanel
variant="sticky"
hasPadding
width={{ default: 'width_25' }}
className="sidebar-panel"
>
<BlueprintsSidebar />
</SidebarPanel>
<SidebarContent>

View file

@ -180,6 +180,43 @@ describe('Blueprints', () => {
await user.keyboard('Milk');
// wait for debounce
await waitFor(
() => {
expect(screen.getAllByRole('radio')).toHaveLength(1);
},
{
timeout: 1500,
}
);
});
});
describe('pagination', () => {
test('paging of blueprints', async () => {
renderWithReduxRouter('', {});
expect(await screen.findAllByRole('radio')).toHaveLength(10);
const option = await screen.findByTestId('blueprints-pagination-bottom');
const prevButton = within(option).getByRole('button', {
name: /Go to previous page/i,
});
const button = within(option).getByRole('button', {
name: /Go to next page/i,
});
expect(prevButton).toBeInTheDocument();
expect(prevButton).toBeVisible();
expect(prevButton).toBeDisabled();
expect(button).toBeInTheDocument();
expect(button).toBeVisible();
await waitFor(() => {
expect(button).toBeEnabled();
});
await user.click(button);
await waitFor(() => {
expect(screen.getAllByRole('radio')).toHaveLength(1);
});

View file

@ -14,7 +14,7 @@ export const mockBlueprintsCreation: CreateBlueprintResponse[] = [
export const mockGetBlueprints: GetBlueprintsApiResponse = {
links: { first: 'first', last: 'last' },
meta: { count: 3 },
meta: { count: 11 },
data: [
{
id: '677b010b-e95e-4694-9813-d11d847f1bfc',
@ -37,6 +37,62 @@ export const mockGetBlueprints: GetBlueprintsApiResponse = {
version: 2,
last_modified_at: '2021-09-08T14:38:00.000Z',
},
{
id: 'b1f10309-a250-4db8-ab64-c110176e3eb7',
name: 'Cupcake',
description: 'Small cake with frosting',
version: 1,
last_modified_at: '2021-09-08T14:38:00.000Z',
},
{
id: '8642171b-d4e5-408b-af9f-68ce8a640df8',
name: 'Salted Caramel Cheesecake',
description: 'Cheesecake topped with salted caramel',
version: 1,
last_modified_at: '2021-09-08T15:12:00.000Z',
},
{
id: 'f460c4eb-0b73-4a56-a1a6-5defc7e29d6b',
name: 'Crustless New York Cheesecake',
description: 'Creamy delicius cheesecake',
version: 1,
last_modified_at: '2021-09-08T16:24:00.000Z',
},
{
id: '366c2c1f-26cd-430a-97a2-f671d7e834b4',
name: 'Fresh Plum Kuchen',
description: 'Kuchen made from the best plums',
version: 1,
last_modified_at: '2021-09-08T17:03:00.000Z',
},
{
id: '3f1a2e77-43b2-467d-b71b-c031ae8f3b7f',
name: 'Chocolate Angel Cake',
description: '70% Dark Chocolate with crunchy cocoa nibs',
version: 1,
last_modified_at: '2021-09-08T18:10:00.000Z',
},
{
id: '689158a7-aa02-4581-b695-6608383477cb',
name: 'Cherry Cola Cake',
description: 'Made from fresh cherries',
version: 1,
last_modified_at: '2021-09-08T19:45:00.000Z',
},
{
id: '6f073028-128d-4e6e-af98-0da2e58c8b60',
name: 'Hummingbird Cake',
description: 'Banana-pineapple spice cake',
version: 1,
last_modified_at: '2021-09-08T20:18:00.000Z',
},
{
id: '147032db-8697-4638-8fdd-6f428100d8fc',
name: 'Red Velvet',
description: 'Layered cake with icing',
version: 1,
last_modified_at: '2021-09-08T21:00:00.000Z',
},
],
};

View file

@ -117,6 +117,8 @@ export const handlers = [
),
rest.get(`${IMAGE_BUILDER_API}/experimental/blueprints`, (req, res, ctx) => {
const search = req.url.searchParams.get('search');
const limit = req.url.searchParams.get('limit') || '10';
const offset = req.url.searchParams.get('offset') || '0';
const resp = Object.assign({}, mockGetBlueprints);
if (search) {
let regexp;
@ -126,11 +128,15 @@ export const handlers = [
const sanitized = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
regexp = new RegExp(sanitized);
}
resp.data = mockGetBlueprints.data.filter(({ name }) => {
resp.data = resp.data.filter(({ name }) => {
return regexp.test(name);
});
resp.meta.count = resp.data.length;
}
resp.meta.count = resp.data.length;
resp.data = resp.data.slice(
parseInt(offset),
parseInt(offset) + parseInt(limit)
);
return res(ctx.status(200), ctx.json(resp));
}),