Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions pages/table/multi-column-sort.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { useCollection } from '@cloudscape-design/collection-hooks';

import { Checkbox, Pagination, SpaceBetween, Table, TableProps } from '~components';

import { useAppContext } from '../app/app-context';
import { SimplePage } from '../app/templates';

interface Item {
id: string;
name: string;
type: string;
state: string;
cpu: number;
}

const allItems: Item[] = [
{ id: '1', name: 'alpha-server', type: 't3.micro', state: 'running', cpu: 12 },
{ id: '2', name: 'beta-server', type: 'm5.large', state: 'stopped', cpu: 0 },
{ id: '3', name: 'gamma-worker', type: 'c5.xlarge', state: 'running', cpu: 87 },
{ id: '4', name: 'delta-proxy', type: 't3.micro', state: 'running', cpu: 45 },
{ id: '5', name: 'epsilon-db', type: 'r5.large', state: 'running', cpu: 23 },
{ id: '6', name: 'zeta-cache', type: 'r5.large', state: 'stopped', cpu: 0 },
{ id: '7', name: 'eta-gateway', type: 't3.micro', state: 'running', cpu: 56 },
{ id: '8', name: 'theta-batch', type: 'c5.xlarge', state: 'running', cpu: 91 },
];

const shortHeaders: Record<string, string> = {
name: 'Name',
type: 'Type',
state: 'State',
cpu: 'CPU %',
};

const longHeaders: Record<string, string> = {
name: 'Fully qualified instance name and identifier',
type: 'Compute instance type and family classification',
state: 'Current lifecycle and operational state',
cpu: 'Average CPU utilization over the last hour (%)',
};

const groupDefinitions: TableProps.GroupDefinition[] = [
{ id: 'identity', header: 'Identity' },
{ id: 'status', header: 'Status' },
];

const groupedColumnDisplay: TableProps.ColumnDisplayProperties[] = [
{
type: 'group',
id: 'identity',
visible: true,
children: [
{ id: 'name', visible: true },
{ id: 'type', visible: true },
],
},
{
type: 'group',
id: 'status',
visible: true,
children: [
{ id: 'state', visible: true },
{ id: 'cpu', visible: true },
],
},
];

const initialMultiColumnSort: ReadonlyArray<TableProps.SortingState<Item>> = [
{ sortingColumn: { sortingField: 'state' }, isDescending: false },
{ sortingColumn: { sortingField: 'cpu' }, isDescending: true },
];

const initialSingleColumnSort: TableProps.SortingState<Item> = initialMultiColumnSort[0];
const PAGE_SIZE = 4;

export default function MultiColumnSortPage() {
const { urlParams, setUrlParams } = useAppContext<
'multiSort' | 'selection' | 'grouped' | 'pagination' | 'longNames' | 'resizable' | 'keyboardNav'
>();

// `multiSort` defaults to on; the rest default to off.
const multiSortEnabled = urlParams.multiSort !== false;
const selectionEnabled = !!urlParams.selection;
const groupedEnabled = !!urlParams.grouped;
const paginationEnabled = !!urlParams.pagination;
const longNamesEnabled = !!urlParams.longNames;
const resizableEnabled = !!urlParams.resizable;
const keyboardNavEnabled = !!urlParams.keyboardNav;

const { items, collectionProps, paginationProps } = useCollection(allItems, {
sorting: multiSortEnabled
? { multiColumn: true, defaultSortingColumns: initialMultiColumnSort }
: { defaultState: initialSingleColumnSort },
pagination: paginationEnabled ? { pageSize: PAGE_SIZE } : undefined,
selection: selectionEnabled ? {} : undefined,
});

const headers = longNamesEnabled ? longHeaders : shortHeaders;
const columnDefinitions: TableProps.ColumnDefinition<Item>[] = [
{
id: 'name',
header: headers.name,
cell: item => item.name,
sortingField: 'name',
},
{
id: 'type',
header: headers.type,
cell: item => item.type,
sortingField: 'type',
},
{
id: 'state',
header: headers.state,
cell: item => item.state,
sortingField: 'state',
},
{
id: 'cpu',
header: headers.cpu,
cell: item => `${item.cpu}%`,
sortingField: 'cpu',
},
];

return (
<SimplePage
title="Multi-column sort"
i18n={{}}
settings={
<SpaceBetween size="l" direction="horizontal">
<Checkbox checked={multiSortEnabled} onChange={({ detail }) => setUrlParams({ multiSort: detail.checked })}>
Multi-column sort
</Checkbox>
<Checkbox checked={selectionEnabled} onChange={({ detail }) => setUrlParams({ selection: detail.checked })}>
Selection
</Checkbox>
<Checkbox checked={groupedEnabled} onChange={({ detail }) => setUrlParams({ grouped: detail.checked })}>
Grouped columns
</Checkbox>
<Checkbox checked={paginationEnabled} onChange={({ detail }) => setUrlParams({ pagination: detail.checked })}>
Pagination
</Checkbox>
<Checkbox checked={longNamesEnabled} onChange={({ detail }) => setUrlParams({ longNames: detail.checked })}>
Long column names
</Checkbox>
<Checkbox checked={resizableEnabled} onChange={({ detail }) => setUrlParams({ resizable: detail.checked })}>
Resizable columns
</Checkbox>
<Checkbox
checked={keyboardNavEnabled}
onChange={({ detail }) => setUrlParams({ keyboardNav: detail.checked })}
>
Keyboard navigation
</Checkbox>
</SpaceBetween>
}
>
<Table
{...collectionProps}
items={items}
columnDefinitions={columnDefinitions}
resizableColumns={resizableEnabled}
enableKeyboardNavigation={keyboardNavEnabled}
groupDefinitions={groupedEnabled ? groupDefinitions : undefined}
columnDisplay={groupedEnabled ? groupedColumnDisplay : undefined}
selectionType={selectionEnabled ? 'multi' : undefined}
pagination={paginationEnabled ? <Pagination {...paginationProps} /> : undefined}
ariaLabels={{
tableLabel: 'Multi-column sort demo',
selectionGroupLabel: 'Item selection',
allItemsSelectionLabel: () => 'Select all',
itemSelectionLabel: (_data, item) => `Select ${item.name}`,
}}
/>
</SimplePage>
);
}
160 changes: 160 additions & 0 deletions pages/table/multi-column-sort.permutations.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Pagination, { PaginationProps } from '~components/pagination';
import Table, { TableProps } from '~components/table';

import { PermutationsPage } from '../app/templates';
import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';

interface Item {
name: string;
type: string;
count: number;
}

const items: Item[] = [
{ name: 'alpha', type: 't3.micro', count: 12 },
{ name: 'beta', type: 'm5.large', count: 3 },
];

const layoutItems: Item[] = [
{
name: 'alpha with a deliberately long resource name that demonstrates the table wrapping behavior',
type: 't3.micro instance type with an intentionally long description for the layout scenarios',
count: 12,
},
{
name: 'beta with another long resource name that demonstrates the table wrapping behavior',
type: 'm5.large instance type with an intentionally long description for the layout scenarios',
count: 3,
},
];

const columnDefinitions: TableProps.ColumnDefinition<Item>[] = [
{ id: 'name', header: 'Name', cell: item => item.name, sortingField: 'name' },
{ id: 'type', header: 'Type', cell: item => item.type, sortingField: 'type' },
{ id: 'count', header: 'Count', cell: item => item.count, sortingField: 'count' },
];

const longColumnDefinitions: TableProps.ColumnDefinition<Item>[] = [
{
id: 'name',
header: 'Resource name with a deliberately long sortable column header',
cell: item => item.name,
sortingField: 'name',
},
{
id: 'type',
header: 'Instance type with a deliberately long sortable column header',
cell: item => item.type,
sortingField: 'type',
},
{
id: 'count',
header: 'Number of associated resources',
cell: item => item.count,
sortingField: 'count',
},
];

const noop = () => {};

const ariaLabels: TableProps.AriaLabels<Item> = {
tableLabel: 'Multi-column sort permutations',
selectionGroupLabel: 'Item selection',
allItemsSelectionLabel: () => 'Select all',
itemSelectionLabel: (_data, item) => `Select ${item.name}`,
};

const paginationLabels: PaginationProps.Labels = {
nextPageLabel: 'Next page',
previousPageLabel: 'Previous page',
pageLabel: pageNumber => `Page ${pageNumber} of all pages`,
};

function getTableLabel(permutation: TableProps<Item>, index: number) {
const sortingCount = permutation.multiColumnSort?.sortingColumns.length ?? 0;
const sortingDescription = sortingCount === 0 ? 'no active sort' : `${sortingCount}-column sort`;
const selectionDescription = permutation.selectionType ? `${permutation.selectionType} selection` : 'no selection';
const resizingDescription = permutation.resizableColumns ? 'resizable columns' : 'fixed columns';
const wrappingDescription = permutation.wrapLines ? 'wrapped lines' : 'unwrapped lines';
const paginationDescription = permutation.pagination ? 'with pagination' : 'no pagination';

return `Multi-column sort example ${index + 1}: ${sortingDescription}, ${selectionDescription}, ${resizingDescription}, ${wrappingDescription}, ${paginationDescription}`;
}

// createPermutations values are rendered by PermutationsView, not React array mapping, so JSX
// nodes used as prop values (e.g. pagination) do not need a key. Matches table/permutations.page.tsx.
/* eslint-disable react/jsx-key */
const permutations = createPermutations<TableProps<Item>>([
{
selectionType: [undefined, 'multi'],
multiColumnSort: [
// No active sort: kebab present, no priority badge, aria-sort="none".
{ sortingColumns: [], onChange: noop },
// Single column: sorted icon, no priority badge (priority adds no info).
{ sortingColumns: [{ sortingColumn: { sortingField: 'name' }, isDescending: false }], onChange: noop },
// Two columns: primary ascending (badge 1, declares aria-sort) + secondary descending (badge 2, suppressed).
{
sortingColumns: [
{ sortingColumn: { sortingField: 'name' }, isDescending: false },
{ sortingColumn: { sortingField: 'type' }, isDescending: true },
],
onChange: noop,
},
],
items: [items],
columnDefinitions: [columnDefinitions],
},
{
selectionType: [undefined],
multiColumnSort: [
{
sortingColumns: [
{ sortingColumn: { sortingField: 'name' }, isDescending: false },
{ sortingColumn: { sortingField: 'type' }, isDescending: true },
],
onChange: noop,
},
],
items: [layoutItems],
columnDefinitions: [longColumnDefinitions],
resizableColumns: [false, true],
wrapLines: [false, true],
},
{
// Pagination alignment: the auto-rendered "Clear sort" button and Pagination share the header tools row.
selectionType: [undefined],
multiColumnSort: [
// Pagination alone (no active sort, so no "Clear sort" button).
{ sortingColumns: [], onChange: noop },
// Active two-column sort renders "Clear sort" alongside Pagination.
{
sortingColumns: [
{ sortingColumn: { sortingField: 'name' }, isDescending: false },
{ sortingColumn: { sortingField: 'type' }, isDescending: true },
],
onChange: noop,
},
],
items: [items],
columnDefinitions: [columnDefinitions],
pagination: [<Pagination currentPageIndex={1} pagesCount={3} ariaLabels={paginationLabels} />],
},
]);

export default function MultiColumnSortPermutationsPage() {
return (
<PermutationsPage title="Table multi-column sort permutations" i18n={{}}>
<PermutationsView
permutations={permutations}
render={(permutation, index = 0) => (
<Table {...permutation} ariaLabels={{ ...ariaLabels, tableLabel: getTableLabel(permutation, index) }} />
)}
/>
</PermutationsPage>
);
}
Loading
Loading