make pagination work on the client list (#39)
* fix: make pagination work * fix formatting * Xmove toolbar to seperate component
This commit is contained in:
parent
64f96c7d1b
commit
f11f2bffdf
6 changed files with 178 additions and 116 deletions
35
src/App.tsx
35
src/App.tsx
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
78
src/components/table-toolbar/TableToolbar.tsx
Normal file
78
src/components/table-toolbar/TableToolbar.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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">
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in a new issue