|
| 1 | +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | +import React, { useContext, useState } from 'react'; |
| 4 | +import clsx from 'clsx'; |
| 5 | + |
| 6 | +import { Checkbox, FormField, Header, Select, SpaceBetween, Table, TableProps } from '~components'; |
| 7 | +import { StyleClassNames } from '~components/table/style-api'; |
| 8 | + |
| 9 | +import AppContext, { AppContextType } from '../app/app-context'; |
| 10 | +import { SimplePage } from '../app/templates'; |
| 11 | +import { generateItems, Instance, InstanceState } from './generate-data'; |
| 12 | +import { columnsConfig } from './shared-configs'; |
| 13 | + |
| 14 | +import styles from './style-api-v2.scss'; |
| 15 | + |
| 16 | +interface Settings { |
| 17 | + selectionType: 'none' | TableProps.SelectionType; |
| 18 | + stickyColumnsFirst: string; |
| 19 | + stickyColumnsLast: string; |
| 20 | + statusHighlight: 'none' | 'cells' | 'rows'; |
| 21 | + nestedRowsHighlight: boolean; |
| 22 | +} |
| 23 | + |
| 24 | +type PageContext = React.Context<AppContextType<Partial<Settings>>>; |
| 25 | + |
| 26 | +const allItems = generateItems(30); |
| 27 | +const rootItems = allItems.slice(0, 10); |
| 28 | +// Four root items have five children each; the first one is expanded by default. |
| 29 | +const parentItems = [0, 1, 3, 5].map(index => rootItems[index]); |
| 30 | +const itemChildren = new Map( |
| 31 | + parentItems.map((item, index) => [item, allItems.slice(10 + index * 5, 15 + index * 5)] as const) |
| 32 | +); |
| 33 | +const nestedItems = new Set([...itemChildren.values()].flat()); |
| 34 | + |
| 35 | +const columnDefinitions = columnsConfig.map(column => ({ ...column, minWidth: 200 })); |
| 36 | +const instanceColumns = ['id', 'type', 'state']; |
| 37 | +const networkColumns = ['dnsName', 'imageId']; |
| 38 | +const columnGroups = new Map([ |
| 39 | + ...instanceColumns.map(id => [id, 'instance'] as const), |
| 40 | + ...networkColumns.map(id => [id, 'network'] as const), |
| 41 | +]); |
| 42 | +const groupDefinitions: TableProps.GroupDefinition<Instance>[] = [ |
| 43 | + { id: 'instance', header: 'Instance' }, |
| 44 | + { id: 'network', header: 'Network' }, |
| 45 | +]; |
| 46 | +const columnDisplay: TableProps.ColumnDisplayProperties[] = [ |
| 47 | + { |
| 48 | + type: 'group', |
| 49 | + id: 'instance', |
| 50 | + visible: true, |
| 51 | + children: instanceColumns.map(id => ({ id, visible: true })), |
| 52 | + }, |
| 53 | + { |
| 54 | + type: 'group', |
| 55 | + id: 'network', |
| 56 | + visible: true, |
| 57 | + children: networkColumns.map(id => ({ id, visible: true })), |
| 58 | + }, |
| 59 | +]; |
| 60 | + |
| 61 | +const ariaLabels: TableProps.AriaLabels<Instance> = { |
| 62 | + selectionGroupLabel: 'Items selection', |
| 63 | + allItemsSelectionLabel: () => 'Select all', |
| 64 | + itemSelectionLabel: (_, item) => `Select ${item.id}`, |
| 65 | + expandButtonLabel: () => 'Expand', |
| 66 | + collapseButtonLabel: () => 'Collapse', |
| 67 | +}; |
| 68 | + |
| 69 | +const selectionTypeOptions = ['none', 'single', 'multi'].map(value => ({ value })); |
| 70 | +const stickyColumnsOptions = ['0', '1', '2'].map(value => ({ value })); |
| 71 | +const statusHighlightOptions = ['none', 'cells', 'rows'].map(value => ({ value })); |
| 72 | + |
| 73 | +const statuses: Partial<Record<InstanceState, string>> = { |
| 74 | + RUNNING: 'running', |
| 75 | + STOPPING: 'transitioning', |
| 76 | + STOPPED: 'stopped', |
| 77 | + TERMINATING: 'transitioning', |
| 78 | + TERMINATED: 'terminated', |
| 79 | +}; |
| 80 | + |
| 81 | +export default function () { |
| 82 | + const { |
| 83 | + urlParams: { |
| 84 | + selectionType = 'multi', |
| 85 | + stickyColumnsFirst = '1', |
| 86 | + stickyColumnsLast = '0', |
| 87 | + statusHighlight = 'cells', |
| 88 | + nestedRowsHighlight = true, |
| 89 | + }, |
| 90 | + setUrlParams, |
| 91 | + } = useContext(AppContext as PageContext); |
| 92 | + |
| 93 | + const [selectedItems, setSelectedItems] = useState<Instance[]>([rootItems[2], rootItems[4]]); |
| 94 | + const [expandedItems, setExpandedItems] = useState<Instance[]>([parentItems[0]]); |
| 95 | + const [sortingColumn, setSortingColumn] = useState<TableProps.SortingColumn<Instance>>(columnDefinitions[1]); |
| 96 | + const [sortingDescending, setSortingDescending] = useState(false); |
| 97 | + |
| 98 | + const sortingField = sortingColumn.sortingField as keyof Instance; |
| 99 | + const items = [...rootItems].sort( |
| 100 | + (a, b) => String(a[sortingField]).localeCompare(String(b[sortingField])) * (sortingDescending ? -1 : 1) |
| 101 | + ); |
| 102 | + |
| 103 | + const booleanSetting = (key: keyof Settings, label: string, checked: boolean) => ( |
| 104 | + <Checkbox checked={checked} onChange={({ detail }) => setUrlParams({ [key]: detail.checked })}> |
| 105 | + {label} |
| 106 | + </Checkbox> |
| 107 | + ); |
| 108 | + const selectSetting = (key: keyof Settings, label: string, value: string, options: { value: string }[]) => ( |
| 109 | + <FormField label={label}> |
| 110 | + <Select |
| 111 | + selectedOption={{ value }} |
| 112 | + options={options} |
| 113 | + onChange={({ detail }) => setUrlParams({ [key]: detail.selectedOption.value })} |
| 114 | + ariaLabel={label} |
| 115 | + /> |
| 116 | + </FormField> |
| 117 | + ); |
| 118 | + |
| 119 | + const statusClassName = (item: Instance, slot: 'row' | 'cell') => |
| 120 | + statuses[item.state] && styles[`status-${statuses[item.state]}-${slot}`]; |
| 121 | + |
| 122 | + const isItemDisabled = (item: Instance) => item.state === 'TERMINATING' || item.state === 'STOPPING'; |
| 123 | + |
| 124 | + // Selected rows keep the selection color: status and nested highlights skip them. |
| 125 | + const styleClassNames: StyleClassNames<Instance> = { |
| 126 | + root: styles.table, |
| 127 | + headerCell: args => { |
| 128 | + if (args.type === 'selection') { |
| 129 | + return clsx(styles['header-cell'], selectedItems.length > 0 && styles['select-all-selected']); |
| 130 | + } |
| 131 | + const groupId = args.type === 'group' ? args.group.id : columnGroups.get(args.column.id ?? ''); |
| 132 | + return groupId ? styles[`${groupId}-header-cell`] : styles['header-cell']; |
| 133 | + }, |
| 134 | + sortingIndicator: styles['sorting-indicator'], |
| 135 | + row: ({ item, selected }) => |
| 136 | + clsx( |
| 137 | + styles.row, |
| 138 | + isItemDisabled(item) && styles['row-disabled'], |
| 139 | + selected |
| 140 | + ? styles['row-selected'] |
| 141 | + : (statusHighlight === 'rows' && statusClassName(item, 'row')) || |
| 142 | + (nestedRowsHighlight && nestedItems.has(item) && styles['row-nested']) |
| 143 | + ), |
| 144 | + cell: args => |
| 145 | + clsx( |
| 146 | + styles.cell, |
| 147 | + statusHighlight === 'cells' && |
| 148 | + args.type === 'data' && |
| 149 | + args.column.id === 'state' && |
| 150 | + !selectedItems.includes(args.item) && |
| 151 | + statusClassName(args.item, 'cell') |
| 152 | + ), |
| 153 | + selectionCheckbox: styles.checkbox, |
| 154 | + selectionRadio: styles.radio, |
| 155 | + }; |
| 156 | + |
| 157 | + return ( |
| 158 | + <SimplePage |
| 159 | + title="Table - Style API v2" |
| 160 | + screenshotArea={{}} |
| 161 | + settings={ |
| 162 | + <SpaceBetween size="m"> |
| 163 | + <SpaceBetween size="m" direction="horizontal" alignItems="end"> |
| 164 | + {selectSetting('selectionType', 'Selection type', selectionType, selectionTypeOptions)} |
| 165 | + {selectSetting('stickyColumnsFirst', 'Sticky columns first', stickyColumnsFirst, stickyColumnsOptions)} |
| 166 | + {selectSetting('stickyColumnsLast', 'Sticky columns last', stickyColumnsLast, stickyColumnsOptions)} |
| 167 | + {selectSetting('statusHighlight', 'Highlight status', statusHighlight, statusHighlightOptions)} |
| 168 | + </SpaceBetween> |
| 169 | + <SpaceBetween size="m" direction="horizontal"> |
| 170 | + {booleanSetting('nestedRowsHighlight', 'Highlight nested rows', nestedRowsHighlight)} |
| 171 | + </SpaceBetween> |
| 172 | + </SpaceBetween> |
| 173 | + } |
| 174 | + > |
| 175 | + <Table |
| 176 | + enableKeyboardNavigation={true} |
| 177 | + header={<Header counter={`(${items.length})`}>Instances</Header>} |
| 178 | + variant="embedded" |
| 179 | + items={items} |
| 180 | + columnDefinitions={columnDefinitions} |
| 181 | + groupDefinitions={groupDefinitions} |
| 182 | + columnDisplay={columnDisplay} |
| 183 | + trackBy="id" |
| 184 | + ariaLabels={ariaLabels} |
| 185 | + resizableColumns={true} |
| 186 | + stickyHeader={true} |
| 187 | + stickyColumns={{ first: +stickyColumnsFirst, last: +stickyColumnsLast }} |
| 188 | + selectionType={selectionType === 'none' ? undefined : selectionType} |
| 189 | + selectedItems={selectedItems} |
| 190 | + onSelectionChange={({ detail }) => setSelectedItems(detail.selectedItems)} |
| 191 | + isItemDisabled={isItemDisabled} |
| 192 | + sortingColumn={sortingColumn} |
| 193 | + sortingDescending={sortingDescending} |
| 194 | + onSortingChange={({ detail }) => { |
| 195 | + setSortingColumn(detail.sortingColumn); |
| 196 | + setSortingDescending(!!detail.isDescending); |
| 197 | + }} |
| 198 | + expandableRows={{ |
| 199 | + getItemChildren: item => itemChildren.get(item) ?? [], |
| 200 | + isItemExpandable: item => itemChildren.has(item), |
| 201 | + expandedItems, |
| 202 | + onExpandableItemToggle: ({ detail }) => |
| 203 | + setExpandedItems(prev => |
| 204 | + detail.expanded ? [...prev, detail.item] : prev.filter(item => item !== detail.item) |
| 205 | + ), |
| 206 | + }} |
| 207 | + {...{ styleClassNames }} |
| 208 | + /> |
| 209 | + </SimplePage> |
| 210 | + ); |
| 211 | +} |
0 commit comments