Skip to content

Commit

Permalink
feat(InfiniteQueryTable): add infinite query table for paginated reac…
Browse files Browse the repository at this point in the history
…t query hooks (#973)

Users of PDK's Type Safe API can make use of react query hooks generated from their
API model. "Infinite query" hooks are generated for paginated operations.

This change adds a table component which accepts the result of such a hook, and
manages pagination by interacting with the hook, simplifying the effort required
to build paginated tables when using Type Safe API and Northstar in tandem.
  • Loading branch information
cogwirrel committed Sep 21, 2023
1 parent 5fe37b5 commit ba0f449
Show file tree
Hide file tree
Showing 7 changed files with 435 additions and 5 deletions.
6 changes: 4 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@
"@storybook/react": "6.5.9",
"@storybook/testing-library": "^0.2.0",
"@storybook/testing-react": "^1.3.0",
"@tanstack/react-query": "^4.35.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/jest-axe": "^3.5.3",
"@types/lodash.orderby": "^4.6.7",
"@types/lodash": "^4.14.198",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-router-dom": "^5",
Expand All @@ -70,7 +71,6 @@
"jest-axe": "^7.0.0",
"jest-environment-jsdom": "^29.6.2",
"license-checker": "^25.0.1",
"lodash.orderby": "^4.6.0",
"merge": "^2.1.1",
"react": "^18",
"react-dom": "^18",
Expand All @@ -96,6 +96,7 @@
"@types/uuid": "^9.0.0",
"amazon-cognito-identity-js": "^6.2.0",
"cross-fetch": "^3.1.6",
"lodash": "^4.17.21",
"react-markdown": "^8.0.5",
"react-qr-code": "^2.0.11",
"remark-frontmatter": "^4.0.1",
Expand All @@ -105,6 +106,7 @@
},
"peerDependencies": {
"@cloudscape-design/components": "^3",
"@tanstack/react-query": "^4.35.3",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-router-dom": "^5",
Expand Down
118 changes: 118 additions & 0 deletions packages/ui/src/components/InfiniteQueryTable/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/** *******************************************************************************************************************
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. *
******************************************************************************************************************** */
import { ComponentStory, Meta } from '@storybook/react';
import InfiniteQueryTable, { InfiniteQueryTableProps } from '.';
import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query';
import { TableProps } from '@cloudscape-design/components/table';

interface TestDataItem {
readonly name: string;
readonly value: number;
}

interface TestDataResponse {
readonly items: TestDataItem[];
readonly nextPageStart?: number;
}

type OmittedProps = 'query' | 'itemsKey' | 'pageSize' | 'columnDefinitions';

export default {
component: InfiniteQueryTable,
title: 'Components/InfiniteQueryTable',
decorators: [
(Story) => (
<QueryClientProvider client={new QueryClient()}>
<Story />
</QueryClientProvider>
),
],
} as Meta<Omit<InfiniteQueryTableProps<TestDataItem, 'items', TestDataItem, unknown>, OmittedProps>>;

const useTestData = () =>
useInfiniteQuery(
['querykey'],
({ pageParam }) => {
return new Promise<TestDataResponse>((resolve) => {
const page = pageParam ?? 0;
setTimeout(
() =>
resolve({
items: [
{ name: `Item ${page + 1}`, value: page + 1 },
{ name: `Item ${page + 2}`, value: page + 2 },
{ name: `Item ${page + 3}`, value: page + 3 },
],
nextPageStart: page + 3,
}),
500
);
});
},
{
getNextPageParam: (res) => res.nextPageStart,
}
);

const columnDefinitions: TableProps.ColumnDefinition<TestDataItem>[] = [
{
id: 'name',
header: 'Name',
cell: (cell) => cell.name,
sortingField: 'name',
},
{
id: 'value',
header: 'Value',
cell: (cell) => cell.value,
sortingField: 'value',
},
];

const Template: ComponentStory<typeof InfiniteQueryTable<TestDataItem, 'items', TestDataItem, unknown>> = (
args: Omit<InfiniteQueryTableProps<TestDataItem, 'items', TestDataItem, unknown>, OmittedProps>
) => {
const query = useTestData();
return (
<InfiniteQueryTable
query={query}
itemsKey={'items'}
pageSize={3}
columnDefinitions={columnDefinitions}
{...args}
/>
);
};

export const Default = Template.bind({});
Default.args = {};

export const ClientSideTextFilter = Template.bind({});
ClientSideTextFilter.args = {
clientSideTextFilter: {
filterFunction: (text, item) => item.name?.includes(text),
placeholder: 'Find items...',
},
};

export const ClientSideSort = Template.bind({});
ClientSideSort.args = {
clientSideSort: {
defaultSortingColumn: {
sortingField: 'name',
},
},
};
86 changes: 86 additions & 0 deletions packages/ui/src/components/InfiniteQueryTable/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/** *******************************************************************************************************************
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. *
******************************************************************************************************************** */
import { act, cleanup, render, waitFor } from '@testing-library/react';
import * as stories from './index.stories';
import { composeStories } from '@storybook/react';
import wrapper from '@cloudscape-design/components/test-utils/dom';
import userEvent from '@testing-library/user-event';

const { Default, ClientSideTextFilter, ClientSideSort } = composeStories(stories);

describe('InfiniteQueryTable', () => {
afterEach(() => {
jest.resetAllMocks();
cleanup();
});

it('should render table', async () => {
const { container } = render(<Default />);
const table = wrapper(container).findTable();
const pagination = wrapper(container).findPagination();

await waitFor(() => expect(table?.findRows()).toHaveLength(3));
expect(table?.findBodyCell(1, 1)?.getElement()).toHaveTextContent('Item 1');
expect(table?.findBodyCell(2, 1)?.getElement()).toHaveTextContent('Item 2');
expect(table?.findBodyCell(3, 1)?.getElement()).toHaveTextContent('Item 3');

await act(() => {
userEvent.click(pagination!.findNextPageButton().getElement());
});

await waitFor(() => expect(table?.findRows()).toHaveLength(3));
expect(table?.findBodyCell(1, 1)?.getElement()).toHaveTextContent('Item 4');
expect(table?.findBodyCell(2, 1)?.getElement()).toHaveTextContent('Item 5');
expect(table?.findBodyCell(3, 1)?.getElement()).toHaveTextContent('Item 6');
});

it('should render with client side filter', async () => {
const { container } = render(<ClientSideTextFilter />);
const table = wrapper(container).findTable();
const search = wrapper(container).findTextFilter();

await waitFor(() => expect(table?.findRows()).toHaveLength(3));
expect(table?.findBodyCell(1, 1)?.getElement()).toHaveTextContent('Item 1');
expect(table?.findBodyCell(2, 1)?.getElement()).toHaveTextContent('Item 2');
expect(table?.findBodyCell(3, 1)?.getElement()).toHaveTextContent('Item 3');

await act(() => {
search?.findInput().setInputValue('3');
});

expect(table?.findRows()).toHaveLength(1);
});

it('should render with client side sorting', async () => {
const { container } = render(<ClientSideSort />);
const table = wrapper(container).findTable();
const sort = table?.findColumnSortingArea(1);

await waitFor(() => expect(table?.findRows()).toHaveLength(3));
expect(table?.findBodyCell(1, 1)?.getElement()).toHaveTextContent('Item 1');
expect(table?.findBodyCell(2, 1)?.getElement()).toHaveTextContent('Item 2');
expect(table?.findBodyCell(3, 1)?.getElement()).toHaveTextContent('Item 3');

await act(() => {
userEvent.click(sort!.getElement());
});

expect(table?.findRows()).toHaveLength(3);
expect(table?.findBodyCell(1, 1)?.getElement()).toHaveTextContent('Item 3');
expect(table?.findBodyCell(2, 1)?.getElement()).toHaveTextContent('Item 2');
expect(table?.findBodyCell(3, 1)?.getElement()).toHaveTextContent('Item 1');
});
});

0 comments on commit ba0f449

Please sign in to comment.