make pagination work on the client list (#39)

* fix: make pagination work

* fix formatting

* Xmove toolbar to seperate component
This commit is contained in:
Erik Jan de Wit 2020-08-24 20:11:17 +02:00 committed by GitHub
parent 64f96c7d1b
commit f11f2bffdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 116 deletions

View file

@ -1,21 +1,24 @@
import React, { useContext } from 'react'; import React, { useContext, useState } from 'react';
import { Page, PageSection, Button } from '@patternfly/react-core';
import { ClientList } from './clients/ClientList'; import { ClientList } from './clients/ClientList';
import { DataLoader } from './components/data-loader/DataLoader'; import { DataLoader } from './components/data-loader/DataLoader';
import { HttpClientContext } from './http-service/HttpClientContext'; import { HttpClientContext } from './http-service/HttpClientContext';
import { Client } from './clients/client-model'; import { Client } from './clients/client-model';
import { Page, PageSection } from '@patternfly/react-core';
import { Header } from './PageHeader'; import { Header } from './PageHeader';
import { PageNav } from './PageNav'; import { PageNav } from './PageNav';
import { KeycloakContext } from './auth/KeycloakContext'; import { KeycloakContext } from './auth/KeycloakContext';
import { TableToolbar } from './components/table-toolbar/TableToolbar';
export const App = () => { export const App = () => {
const [max, setMax] = useState(10);
const [first, setFirst] = useState(0);
const httpClient = useContext(HttpClientContext); const httpClient = useContext(HttpClientContext);
const keycloak = useContext(KeycloakContext); const keycloak = useContext(KeycloakContext);
const loader = async () => { const loader = async () => {
return await httpClient return await httpClient
?.doGet('/realms/master/clients?first=0&max=20&search=true') ?.doGet('/realms/master/clients', { params: { first, max } })
.then((r) => r.data as Client[]); .then((r) => r.data as Client[]);
}; };
return ( return (
@ -23,10 +26,28 @@ export const App = () => {
<PageSection variant="light"> <PageSection variant="light">
<DataLoader loader={loader}> <DataLoader loader={loader}>
{(clients) => ( {(clients) => (
<ClientList <TableToolbar
clients={clients} count={clients!.length}
baseUrl={keycloak!.authServerUrl()!} first={first}
/> max={max}
onNextClick={(f) => setFirst(f)}
onPreviousClick={(f) => setFirst(f)}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button>Create client</Button>
<Button variant="link">Import client</Button>
</>
}
>
<ClientList
clients={clients}
baseUrl={keycloak!.authServerUrl()!}
/>
</TableToolbar>
)} )}
</DataLoader> </DataLoader>
</PageSection> </PageSection>

View file

@ -1,19 +1,20 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Avatar, Avatar,
Button, Button,
ButtonVariant, ButtonVariant,
Brand, Brand,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownSeparator, DropdownSeparator,
DropdownToggle, DropdownToggle,
KebabToggle, KebabToggle,
PageHeader, PageHeader,
PageHeaderTools, PageHeaderTools,
PageHeaderToolsItem, PageHeaderToolsItem,
PageHeaderToolsGroup} from '@patternfly/react-core'; PageHeaderToolsGroup,
} from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons'; import { HelpIcon } from '@patternfly/react-icons';
import { KeycloakContext } from './auth/KeycloakContext'; import { KeycloakContext } from './auth/KeycloakContext';
@ -31,46 +32,51 @@ const ManageAccountDropdownItem = () => {
const keycloak = useContext(KeycloakContext); const keycloak = useContext(KeycloakContext);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<DropdownItem key="manage account" onClick={() => keycloak?.account()}>{t('Manage account')}</DropdownItem> <DropdownItem key="manage account" onClick={() => keycloak?.account()}>
{t('Manage account')}
</DropdownItem>
); );
} };
const SignOutDropdownItem = () => { const SignOutDropdownItem = () => {
const keycloak = useContext(KeycloakContext); const keycloak = useContext(KeycloakContext);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<DropdownItem key="sign out" onClick={() => keycloak?.logout()}>{t('Sign out')}</DropdownItem> <DropdownItem key="sign out" onClick={() => keycloak?.logout()}>
{t('Sign out')}
</DropdownItem>
); );
} };
const ServerInfoDropdownItem = () => { const ServerInfoDropdownItem = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return <DropdownItem key="server info">{t('Server info')}</DropdownItem>;
<DropdownItem key="server info">{t('Server info')}</DropdownItem> };
)
}
const HelpDropdownItem = () => { const HelpDropdownItem = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const help = t('Help'); const help = t('Help');
return ( return (
<DropdownItem><HelpIcon />{` ${help}`}</DropdownItem> <DropdownItem>
) <HelpIcon />
} {` ${help}`}
</DropdownItem>
);
};
const kebabDropdownItems = [ const kebabDropdownItems = [
<ManageAccountDropdownItem key='kebab Manage Account'/>, <ManageAccountDropdownItem key="kebab Manage Account" />,
<ServerInfoDropdownItem key='kebab Server Info'/>, <ServerInfoDropdownItem key="kebab Server Info" />,
<HelpDropdownItem key='kebab Help'/>, <HelpDropdownItem key="kebab Help" />,
<DropdownSeparator key="kebab sign out seperator" />, <DropdownSeparator key="kebab sign out seperator" />,
<SignOutDropdownItem key='kebab Sign out'/> <SignOutDropdownItem key="kebab Sign out" />,
]; ];
const userDropdownItems = [ const userDropdownItems = [
<ManageAccountDropdownItem key='Manage Account'/>, <ManageAccountDropdownItem key="Manage Account" />,
<ServerInfoDropdownItem key='Server info'/>, <ServerInfoDropdownItem key="Server info" />,
<DropdownSeparator key="sign out seperator" />, <DropdownSeparator key="sign out seperator" />,
<SignOutDropdownItem key='Sign out'/> <SignOutDropdownItem key="Sign out" />,
]; ];
const headerTools = () => { const headerTools = () => {
@ -79,7 +85,7 @@ const headerTools = () => {
<PageHeaderToolsGroup <PageHeaderToolsGroup
visibility={{ visibility={{
default: 'hidden', default: 'hidden',
md: 'visible' md: 'visible',
}} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */ }} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */
> >
<PageHeaderToolsItem> <PageHeaderToolsItem>
@ -92,24 +98,24 @@ const headerTools = () => {
<PageHeaderToolsGroup> <PageHeaderToolsGroup>
<PageHeaderToolsItem <PageHeaderToolsItem
visibility={{ visibility={{
md: 'hidden' md: 'hidden',
}} /** this kebab dropdown replaces the icon buttons and is hidden for desktop sizes */ }} /** this kebab dropdown replaces the icon buttons and is hidden for desktop sizes */
> >
<KebabDropdown/> <KebabDropdown />
</PageHeaderToolsItem> </PageHeaderToolsItem>
<PageHeaderToolsItem <PageHeaderToolsItem
visibility={{ visibility={{
default: 'hidden', default: 'hidden',
md: 'visible' md: 'visible',
}} /** this user dropdown is hidden on mobile sizes */ }} /** this user dropdown is hidden on mobile sizes */
> >
<UserDropdown/> <UserDropdown />
</PageHeaderToolsItem> </PageHeaderToolsItem>
</PageHeaderToolsGroup> </PageHeaderToolsGroup>
<Avatar src="/img_avatar.svg" alt="Avatar image" /> <Avatar src="/img_avatar.svg" alt="Avatar image" />
</PageHeaderTools> </PageHeaderTools>
); );
} };
const KebabDropdown = () => { const KebabDropdown = () => {
const [isDropdownOpen, setDropdownOpen] = useState(false); const [isDropdownOpen, setDropdownOpen] = useState(false);
@ -126,8 +132,8 @@ const KebabDropdown = () => {
isOpen={isDropdownOpen} isOpen={isDropdownOpen}
dropdownItems={kebabDropdownItems} dropdownItems={kebabDropdownItems}
/> />
) );
} };
const UserDropdown = () => { const UserDropdown = () => {
const keycloak = useContext(KeycloakContext); const keycloak = useContext(KeycloakContext);
@ -142,8 +148,12 @@ const UserDropdown = () => {
isPlain isPlain
position="right" position="right"
isOpen={isDropdownOpen} isOpen={isDropdownOpen}
toggle={<DropdownToggle onToggle={onDropdownToggle}>{keycloak?.loggedInUser}</DropdownToggle>} toggle={
<DropdownToggle onToggle={onDropdownToggle}>
{keycloak?.loggedInUser}
</DropdownToggle>
}
dropdownItems={userDropdownItems} dropdownItems={userDropdownItems}
/> />
); );
} };

View file

@ -7,18 +7,7 @@ import {
IFormatter, IFormatter,
IFormatterValueType, IFormatterValueType,
} from '@patternfly/react-table'; } from '@patternfly/react-table';
import { import { Badge } from '@patternfly/react-core';
ToolbarContent,
ToolbarItem,
Pagination,
Toolbar,
InputGroup,
TextInput,
Button,
Badge,
ToggleTemplateProps,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import { Client } from './client-model'; import { Client } from './client-model';
@ -35,21 +24,6 @@ const columns: (keyof Client)[] = [
]; ];
export const ClientList = ({ baseUrl, clients }: ClientListProps) => { export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
const pagination = (variant: 'top' | 'bottom' = 'top') => (
<Pagination
isCompact
toggleTemplate={({ firstIndex, lastIndex }: ToggleTemplateProps) => (
<b>
{firstIndex} - {lastIndex}
</b>
)}
itemCount={100}
page={1}
perPage={10}
variant={variant}
/>
);
const enabled = (): IFormatter => (data?: IFormatterValueType) => { const enabled = (): IFormatter => (data?: IFormatterValueType) => {
const field = data!.toString(); const field = data!.toString();
const value = field.substring(0, field.indexOf('#')); const value = field.substring(0, field.indexOf('#'));
@ -83,41 +57,19 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
return { cells: columns.map((col) => c[col]) }; return { cells: columns.map((col) => c[col]) };
}); });
return ( return (
<> <Table
<Toolbar> variant={TableVariant.compact}
<ToolbarContent> cells={[
<ToolbarItem> { title: 'Client ID', cellFormatters: [enabled()] },
<InputGroup> 'Type',
<TextInput type="text" aria-label="search for client criteria" /> { title: 'Description', cellFormatters: [emptyFormatter()] },
<Button variant="control" aria-label="search for client"> { title: 'Home URL', cellFormatters: [emptyFormatter()] },
<SearchIcon /> ]}
</Button> rows={data}
</InputGroup> aria-label="Client list"
</ToolbarItem> >
<ToolbarItem> <TableHeader />
<Button>Create client</Button> <TableBody />
<Button variant="link">Import client</Button> </Table>
</ToolbarItem>
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
</ToolbarContent>
</Toolbar>
<Table
variant={TableVariant.compact}
cells={[
{ title: 'Client ID', cellFormatters: [enabled()] },
'Type',
{ title: 'Description', cellFormatters: [emptyFormatter()] },
{ title: 'Home URL', cellFormatters: [emptyFormatter()] },
]}
rows={data}
aria-label="Client list"
>
<TableHeader />
<TableBody />
</Table>
<Toolbar>
<ToolbarItem>{pagination('bottom')}</ToolbarItem>
</Toolbar>
</>
); );
}; };

View file

@ -0,0 +1,78 @@
import React from 'react';
import {
ToggleTemplateProps,
Toolbar,
ToolbarContent,
ToolbarItem,
InputGroup,
TextInput,
Button,
Pagination,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
type TableToolbarProps = {
count: number;
first: number;
max: number;
onNextClick: (page: number) => void;
onPreviousClick: (page: number) => void;
onPerPageSelect: (max: number, first: number) => void;
toolbarItem?: React.ReactNode;
children: React.ReactNode;
};
export const TableToolbar = ({
count,
first,
max,
onNextClick,
onPreviousClick,
onPerPageSelect,
toolbarItem,
children,
}: TableToolbarProps) => {
const page = first / max;
const pagination = (variant: 'top' | 'bottom' = 'top') => (
<Pagination
isCompact
toggleTemplate={({ firstIndex, lastIndex }: ToggleTemplateProps) => (
<b>
{firstIndex} - {lastIndex}
</b>
)}
itemCount={count + page * max + (count <= max ? 1 : 0)}
page={page + 1}
perPage={max}
onNextClick={(_, p) => onNextClick((p - 1) * max)}
onPreviousClick={(_, p) => onPreviousClick((p - 1) * max)}
onPerPageSelect={(_, m, f) => onPerPageSelect(f, m)}
variant={variant}
/>
);
return (
<>
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<InputGroup>
<TextInput type="text" aria-label="search for client criteria" />
<Button variant="control" aria-label="search for client">
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
{ toolbarItem && <ToolbarItem>
{ toolbarItem }
</ToolbarItem>}
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
</ToolbarContent>
</Toolbar>
{children}
<Toolbar>
<ToolbarItem>{pagination('bottom')}</ToolbarItem>
</Toolbar>
</>
);
};

View file

@ -1,4 +1,4 @@
import React from "react"; import React from 'react';
import { import {
Text, Text,
PageSection, PageSection,
@ -11,13 +11,14 @@ import {
ActionGroup, ActionGroup,
Button, Button,
Divider, Divider,
} from "@patternfly/react-core"; } from '@patternfly/react-core';
//type NewRealmFormProps = { //type NewRealmFormProps = {
// realm: string; // realm: string;
//}; //};
export const NewRealmForm = () => { //({ realm }: NewRealmFormProps) => { export const NewRealmForm = () => {
//({ realm }: NewRealmFormProps) => {
return ( return (
<> <>
<PageSection variant="light"> <PageSection variant="light">

View file

@ -114,7 +114,7 @@ export class HttpClient {
); );
} }
return url + searchParams.toString(); return url + '?' + searchParams.toString();
} }
private makeConfig(config: RequestInit = {}): Promise<RequestInit> { private makeConfig(config: RequestInit = {}): Promise<RequestInit> {