Skip to content

Commit

Permalink
Merge pull request #808 from kinvolk/simple-table-details-from-url
Browse files Browse the repository at this point in the history
Reflect table pages in the URL
  • Loading branch information
joaquimrocha authored Nov 22, 2022
2 parents f7461c7 + a627757 commit dc5e363
Show file tree
Hide file tree
Showing 19 changed files with 612 additions and 42 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ backend/tools
app/electron/src/*
docs/development/storybook/
.plugins
node_modules
9 changes: 9 additions & 0 deletions docs/development/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ From within the [Headlamp](https://github.com/kinvolk/headlamp/) repo run:
make storybook
```

If you are adding new stories, please wrap your story components with the `TestContext` helper
component as it sets up the store, memory router, and other utilities that may be needed for
current or future stories:

```jsx
<TestContext>
<YourComponentTheStoryIsAbout />
</TestContext>
```

## Accessibility (a11y)

Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/common/Resource/ResourceTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function Table(props: ResourceTableProps) {
rowsPerPage={[15, 25, 50]}
defaultSortingColumn={sortingColumn}
filterFunction={useFilterFunc()}
reflectInURL
{...otherProps}
/>
);
Expand Down
100 changes: 98 additions & 2 deletions frontend/src/components/common/SimpleTable.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Box, Typography } from '@material-ui/core';
import { Meta, Story } from '@storybook/react/types-6-0';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { createStore } from 'redux';
import { KubeObjectInterface } from '../../lib/k8s/cluster';
import { useFilterFunc } from '../../lib/util';
import store from '../../redux/stores/store';
import { TestContext, TestContextProps } from '../../test';
import SectionFilterHeader from './SectionFilterHeader';
import SimpleTable, { SimpleTableProps } from './SimpleTable';

Expand All @@ -14,10 +16,27 @@ export default {
argTypes: {},
} as Meta;

function TestSimpleTable(props: SimpleTableProps) {
const location = useLocation();
if (!!props.reflectInURL) {
return (
<Box>
<Typography>Test changing the page and rows per page.</Typography>
<Typography>
<b>Current URL search:</b> {`${location.search || ''}`}
</Typography>
<SimpleTable {...props} />
</Box>
);
}

return <SimpleTable {...props} />;
}

const Template: Story<SimpleTableProps> = args => (
<MemoryRouter>
<Provider store={store}>
<SimpleTable {...args} />
<TestSimpleTable {...args} />
</Provider>
</MemoryRouter>
);
Expand Down Expand Up @@ -131,6 +150,83 @@ Datum.args = {
],
};

const TemplateWithURLReflection: Story<{
simpleTableProps: SimpleTableProps;
testContextProps: TestContextProps;
}> = args => {
const { testContextProps, simpleTableProps } = args;
return (
<TestContext {...testContextProps}>
<TestSimpleTable {...simpleTableProps} />
</TestContext>
);
};

export const ReflectInURL = TemplateWithURLReflection.bind({});
const lotsOfData = (() => {
const data = [];
for (let i = 0; i < 50; i++) {
data.push({
name: `Name ${i}`,
namespace: `Namespace ${i}`,
number: i,
});
}
return data;
})();
ReflectInURL.args = {
simpleTableProps: {
data: lotsOfData,
columns: [
{
label: 'Name',
datum: 'name',
},
{
label: 'Namespace',
datum: 'namespace',
},
{
label: 'Number',
datum: 'number',
},
],
rowsPerPage: [5, 10, 15],
reflectInURL: true,
showPagination: !process.env.UNDER_TEST, // Disable for snapshots: The pagination uses useId so snapshots will fail.
},
testContextProps: {
urlSearchParams: { p: '2' }, // 2nd page
},
};

export const ReflectInURLWithPrefix = TemplateWithURLReflection.bind({});
ReflectInURLWithPrefix.args = {
simpleTableProps: {
data: lotsOfData,
columns: [
{
label: 'Name',
datum: 'name',
},
{
label: 'Namespace',
datum: 'namespace',
},
{
label: 'Number',
datum: 'creationDate',
},
],
rowsPerPage: [5, 10, 15],
reflectInURL: 'mySuperTable',
showPagination: !process.env.UNDER_TEST, // Disable for snapshots: The pagination uses useId so snapshots will fail.
},
testContextProps: {
urlSearchParams: { p: '2' }, // 2nd page
},
};

// filter Function

type SimpleTableWithFilterProps = SimpleTableProps & { matchCriteria?: string[] };
Expand Down
71 changes: 64 additions & 7 deletions frontend/src/components/common/SimpleTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import TableRow from '@material-ui/core/TableRow';
import React from 'react';
import { useTranslation } from 'react-i18next';
import helpers from '../../helpers';
import { useURLState } from '../../lib/util';
import Empty from './EmptyContent';
import { ValueLabel } from './Label';
import Loader from './Loader';
Expand Down Expand Up @@ -76,6 +77,15 @@ export interface SimpleTableProps {
errorMessage?: string | null;
defaultSortingColumn?: number;
noTableHeader?: boolean;
/** Whether to reflect the page/perPage properties in the URL.
* If assigned to a string, it will be the prefix for the page/perPage parameters.
* If true or '', it'll reflect the parameters without a prefix.
* By default, no parameters are reflected in the URL. */
reflectInURL?: string | boolean;
/** The page number to show by default (by default it's the first page). */
page?: number;
/** Whether to show the pagination component */
showPagination?: boolean;
}

interface ColumnSortButtonProps {
Expand Down Expand Up @@ -106,23 +116,60 @@ function ColumnSortButtons(props: ColumnSortButtonProps) {
);
}

// Use a zero-indexed "useURLState" hook, so pages are shown in the URL as 1-indexed
// but internally are 0-indexed.
function usePageURLState(
key: string,
prefix: string,
initialPage: number
): ReturnType<typeof useURLState> {
const [page, setPage] = useURLState(key, { defaultValue: initialPage + 1, prefix });
const [zeroIndexPage, setZeroIndexPage] = React.useState(page - 1);

React.useEffect(() => {
setZeroIndexPage((zeroIndexPage: number) => {
if (page - 1 !== zeroIndexPage) {
return page - 1;
}

return zeroIndexPage;
});
}, [page]);

React.useEffect(() => {
setPage(zeroIndexPage + 1);
}, [zeroIndexPage]);

return [zeroIndexPage, setZeroIndexPage];
}

export default function SimpleTable(props: SimpleTableProps) {
const {
columns,
data,
filterFunction = null,
emptyMessage = null,
page: initialPage = 0,
showPagination = true,
errorMessage = null,
defaultSortingColumn,
noTableHeader = false,
reflectInURL,
} = props;
const [page, setPage] = React.useState(0);
const shouldReflectInURL = reflectInURL !== undefined && reflectInURL !== false;
const prefix = reflectInURL === true ? '' : reflectInURL || '';
const [page, setPage] = usePageURLState(shouldReflectInURL ? 'p' : '', prefix, initialPage);
const [currentData, setCurrentData] = React.useState(data);
const [displayData, setDisplayData] = React.useState(data);
const rowsPerPageOptions = props.rowsPerPage || [15, 25, 50];
const [rowsPerPage, setRowsPerPage] = React.useState(
helpers.getTablesRowsPerPage(rowsPerPageOptions[0])
const defaultRowsPerPage = React.useMemo(
() => helpers.getTablesRowsPerPage(rowsPerPageOptions[0]),
[]
);
const [rowsPerPage, setRowsPerPage] = useURLState(shouldReflectInURL ? 'perPage' : '', {
defaultValue: defaultRowsPerPage,
prefix,
});
const classes = useTableStyle();
const [isIncreasingOrder, setIsIncreasingOrder] = React.useState(
!defaultSortingColumn || defaultSortingColumn > 0
Expand All @@ -137,6 +184,18 @@ export default function SimpleTable(props: SimpleTableProps) {
setPage(newPage);
}

// Protect against invalid page values
React.useEffect(() => {
if (page < 0) {
setPage(0);
return;
}

if (displayData && page * rowsPerPage > displayData.length) {
setPage(Math.floor(displayData.length / rowsPerPage));
}
}, [page, displayData, rowsPerPage]);

function handleChangeRowsPerPage(
event: React.ChangeEvent<HTMLTextAreaElement> | React.ChangeEvent<HTMLInputElement>
) {
Expand All @@ -149,9 +208,6 @@ export default function SimpleTable(props: SimpleTableProps) {
React.useEffect(
() => {
if (currentData === data) {
if (page !== 0) {
setPage(0);
}
return;
}

Expand Down Expand Up @@ -270,6 +326,7 @@ export default function SimpleTable(props: SimpleTableProps) {
startIcon={<Icon icon="mdi:refresh" />}
onClick={() => {
setCurrentData(data);
setPage(0);
}}
>
{t('frequent|Refresh')}
Expand Down Expand Up @@ -334,7 +391,7 @@ export default function SimpleTable(props: SimpleTableProps) {
)}
</TableBody>
</Table>
{filteredData.length > rowsPerPageOptions[0] && (
{filteredData.length > rowsPerPageOptions[0] && showPagination && (
<TablePagination
rowsPerPageOptions={rowsPerPageOptions}
component="div"
Expand Down
Loading

0 comments on commit dc5e363

Please sign in to comment.