Skip to content

Commit

Permalink
Add client-side pagination
Browse files Browse the repository at this point in the history
Add client-side pagination to all list pages via the `ListPageLayout`
container. Update the `children` prop to expect a function so we can
use the render props pattern to allow the `ListPageLayout` component
to handle all of the pagination logic required.

Consuming pages pass their raw resources to `ListPageLayout` and it
passes back the correct slice for the current pagination settings
which the consumer then formats / renders as needed.

Although we're still loading and storing the full list of resources
from the API this does have significant performance benefits in the
client, especially with larger number of resources. Notably, this
addresses freezing reported by a number of users on the PipelineRuns
and TaskRuns pages with busy clusters.
  • Loading branch information
AlanGreene authored and tekton-robot committed Apr 26, 2022
1 parent f86bf57 commit 14cce74
Show file tree
Hide file tree
Showing 29 changed files with 1,015 additions and 794 deletions.
89 changes: 46 additions & 43 deletions src/containers/ClusterInterceptors/ClusterInterceptors.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ import { Link as CarbonLink } from 'carbon-components-react';
import { ListPageLayout } from '..';
import { useClusterInterceptors } from '../../api';

function getFormattedResources(resources) {
return resources.map(clusterInterceptor => ({
id: clusterInterceptor.metadata.uid,
name: (
<Link
component={CarbonLink}
to={urls.rawCRD.cluster({
name: clusterInterceptor.metadata.name,
type: 'clusterinterceptors'
})}
title={clusterInterceptor.metadata.name}
>
{clusterInterceptor.metadata.name}
</Link>
),
createdTime: (
<FormattedDate
date={clusterInterceptor.metadata.creationTimestamp}
relative
/>
)
}));
}

function ClusterInterceptors({ intl }) {
const location = useLocation();
const filters = getFilters(location);
Expand Down Expand Up @@ -67,56 +91,35 @@ function ClusterInterceptors({ intl }) {
}
];

const clusterInterceptorsFormatted = clusterInterceptors.map(
clusterInterceptor => ({
id: clusterInterceptor.metadata.uid,
name: (
<Link
component={CarbonLink}
to={urls.rawCRD.cluster({
name: clusterInterceptor.metadata.name,
type: 'clusterinterceptors'
})}
title={clusterInterceptor.metadata.name}
>
{clusterInterceptor.metadata.name}
</Link>
),
createdTime: (
<FormattedDate
date={clusterInterceptor.metadata.creationTimestamp}
relative
/>
)
})
);

return (
<ListPageLayout
error={getError()}
filters={filters}
hideNamespacesDropdown
resources={clusterInterceptors}
title="ClusterInterceptors"
>
<Table
headers={headers}
rows={clusterInterceptorsFormatted}
loading={isLoading}
emptyTextAllNamespaces={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterInterceptors' }
)}
emptyTextSelectedNamespace={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterInterceptors' }
)}
/>
{({ resources }) => (
<Table
headers={headers}
rows={getFormattedResources(resources)}
loading={isLoading}
emptyTextAllNamespaces={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterInterceptors' }
)}
emptyTextSelectedNamespace={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterInterceptors' }
)}
/>
)}
</ListPageLayout>
);
}
Expand Down
243 changes: 130 additions & 113 deletions src/containers/ClusterTasks/ClusterTasks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2021 The Tekton Authors
Copyright 2019-2022 The Tekton Authors
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
Expand Down Expand Up @@ -32,6 +32,95 @@ import { getFilters, urls, useTitleSync } from '@tektoncd/dashboard-utils';
import { ListPageLayout } from '..';
import { deleteClusterTask, useClusterTasks, useIsReadOnly } from '../../api';

function getFormattedResources({
intl,
isReadOnly,
openDeleteModal,
resources
}) {
return resources.map(clusterTask => ({
id: clusterTask.metadata.uid,
name: (
<Link
component={CarbonLink}
to={urls.rawCRD.cluster({
type: 'clustertasks',
name: clusterTask.metadata.name
})}
title={clusterTask.metadata.name}
>
{clusterTask.metadata.name}
</Link>
),
createdTime: (
<FormattedDate date={clusterTask.metadata.creationTimestamp} relative />
),
actions: (
<>
{!isReadOnly ? (
<Button
className="tkn--danger"
hasIconOnly
iconDescription={intl.formatMessage({
id: 'dashboard.actions.deleteButton',
defaultMessage: 'Delete'
})}
kind="ghost"
onClick={() =>
openDeleteModal([{ id: clusterTask.metadata.uid }], () => {})
}
renderIcon={DeleteIcon}
size="sm"
tooltipAlignment="center"
tooltipPosition="left"
/>
) : null}
{!isReadOnly ? (
<Button
as={Link}
hasIconOnly
iconDescription={intl.formatMessage(
{
id: 'dashboard.actions.createRunButton',
defaultMessage: 'Create {kind}'
},
{ kind: 'TaskRun' }
)}
kind="ghost"
renderIcon={RunIcon}
size="sm"
to={`${urls.taskRuns.create()}?${new URLSearchParams({
kind: 'ClusterTask',
taskName: clusterTask.metadata.name
}).toString()}`}
tooltipAlignment="center"
tooltipPosition="left"
/>
) : null}
<Button
as={Link}
hasIconOnly
iconDescription={intl.formatMessage(
{
id: 'dashboard.resourceList.viewRuns',
defaultMessage: 'View {kind} of {resource}'
},
{ kind: 'TaskRuns', resource: clusterTask.metadata.name }
)}
kind="ghost"
renderIcon={RunsIcon}
size="sm"
to={urls.taskRuns.byClusterTask({
taskName: clusterTask.metadata.name
})}
tooltipAlignment="center"
tooltipPosition="left"
/>
</>
)
}));
}

function ClusterTasksContainer({ intl }) {
const location = useLocation();
const [cancelSelection, setCancelSelection] = useState(null);
Expand Down Expand Up @@ -141,125 +230,53 @@ function ClusterTasksContainer({ intl }) {
}
];

const clusterTasksFormatted = clusterTasks.map(clusterTask => ({
id: clusterTask.metadata.uid,
name: (
<Link
component={CarbonLink}
to={urls.rawCRD.cluster({
type: 'clustertasks',
name: clusterTask.metadata.name
})}
title={clusterTask.metadata.name}
>
{clusterTask.metadata.name}
</Link>
),
createdTime: (
<FormattedDate date={clusterTask.metadata.creationTimestamp} relative />
),
actions: (
<>
{!isReadOnly ? (
<Button
className="tkn--danger"
hasIconOnly
iconDescription={intl.formatMessage({
id: 'dashboard.actions.deleteButton',
defaultMessage: 'Delete'
})}
kind="ghost"
onClick={() =>
openDeleteModal([{ id: clusterTask.metadata.uid }], () => {})
}
renderIcon={DeleteIcon}
size="sm"
tooltipAlignment="center"
tooltipPosition="left"
/>
) : null}
{!isReadOnly ? (
<Button
as={Link}
hasIconOnly
iconDescription={intl.formatMessage(
{
id: 'dashboard.actions.createRunButton',
defaultMessage: 'Create {kind}'
},
{ kind: 'TaskRun' }
)}
kind="ghost"
renderIcon={RunIcon}
size="sm"
to={`${urls.taskRuns.create()}?${new URLSearchParams({
kind: 'ClusterTask',
taskName: clusterTask.metadata.name
}).toString()}`}
tooltipAlignment="center"
tooltipPosition="left"
/>
) : null}
<Button
as={Link}
hasIconOnly
iconDescription={intl.formatMessage(
{
id: 'dashboard.resourceList.viewRuns',
defaultMessage: 'View {kind} of {resource}'
},
{ kind: 'TaskRuns', resource: clusterTask.metadata.name }
)}
kind="ghost"
renderIcon={RunsIcon}
size="sm"
to={urls.taskRuns.byClusterTask({
taskName: clusterTask.metadata.name
})}
tooltipAlignment="center"
tooltipPosition="left"
/>
</>
)
}));

return (
<ListPageLayout
error={getError()}
filters={filters}
hideNamespacesDropdown
resources={clusterTasks}
title="ClusterTasks"
>
<Table
batchActionButtons={batchActionButtons}
className="tkn--table--inline-actions"
headers={initialHeaders}
rows={clusterTasksFormatted}
loading={isLoading}
emptyTextAllNamespaces={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterTasks' }
)}
emptyTextSelectedNamespace={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterTasks' }
)}
/>
{showDeleteModal ? (
<DeleteModal
kind="ClusterTasks"
onClose={closeDeleteModal}
onSubmit={handleDelete}
resources={toBeDeleted}
showNamespace={false}
/>
) : null}
{({ resources }) => (
<>
<Table
batchActionButtons={batchActionButtons}
className="tkn--table--inline-actions"
headers={initialHeaders}
rows={getFormattedResources({
intl,
isReadOnly,
openDeleteModal,
resources
})}
loading={isLoading}
emptyTextAllNamespaces={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterTasks' }
)}
emptyTextSelectedNamespace={intl.formatMessage(
{
id: 'dashboard.emptyState.clusterResource',
defaultMessage: 'No matching {kind} found'
},
{ kind: 'ClusterTasks' }
)}
/>
{showDeleteModal ? (
<DeleteModal
kind="ClusterTasks"
onClose={closeDeleteModal}
onSubmit={handleDelete}
resources={toBeDeleted}
showNamespace={false}
/>
) : null}
</>
)}
</ListPageLayout>
);
}
Expand Down
Loading

0 comments on commit 14cce74

Please sign in to comment.