feat(HMS-3401): Add blueprints sidebar pagination
This commit is contained in:
parent
7bff1feaf4
commit
a3a7ea88c5
9 changed files with 202 additions and 7 deletions
56
src/Components/Blueprints/BlueprintsPagination.tsx
Normal file
56
src/Components/Blueprints/BlueprintsPagination.tsx
Normal 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;
|
||||
|
|
@ -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),
|
||||
[]
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -9,3 +9,9 @@
|
|||
.expand-section {
|
||||
background-color: var(--pf-global--palette--white);
|
||||
}
|
||||
|
||||
.sidebar-panel {
|
||||
height: 750px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
58
src/test/fixtures/blueprints.ts
vendored
58
src/test/fixtures/blueprints.ts
vendored
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue