|
| 1 | +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +/** |
| 5 | + * CloudWatch Logs Insights–style demo exercising R1 atomic table features: |
| 6 | + * - Sticky header with page-scroll pinning and horizontal-scroll alignment |
| 7 | + * - Grid columnLayout with explicit column widths (simulating query results) |
| 8 | + * - Row selection with multi-select checkboxes |
| 9 | + * - Keyboard-accessible horizontal scroll region (role='region' when overflow) |
| 10 | + * - Large dataset (500 rows) simulating query results |
| 11 | + */ |
| 12 | +import React, { useMemo, useState } from 'react'; |
| 13 | + |
| 14 | +import Box from '~components/box'; |
| 15 | +import Checkbox from '~components/checkbox'; |
| 16 | +import Container from '~components/container'; |
| 17 | +import Header from '~components/header'; |
| 18 | +import SegmentedControl from '~components/segmented-control'; |
| 19 | +import SpaceBetween from '~components/space-between'; |
| 20 | +import StatusIndicator from '~components/status-indicator'; |
| 21 | +import TableBody from '~components/table-body'; |
| 22 | +import TableBodyCell from '~components/table-body-cell'; |
| 23 | +import TableHead from '~components/table-head'; |
| 24 | +import TableHeaderCell from '~components/table-header-cell'; |
| 25 | +import TableRoot, { TableRootProps } from '~components/table-root'; |
| 26 | +import TableRow from '~components/table-row'; |
| 27 | + |
| 28 | +import styles from './styles.scss'; |
| 29 | + |
| 30 | +const LOG_LEVELS = ['INFO', 'WARN', 'ERROR', 'DEBUG'] as const; |
| 31 | +type LogLevel = (typeof LOG_LEVELS)[number]; |
| 32 | + |
| 33 | +const LAMBDA_FUNCTIONS = [ |
| 34 | + 'process-orders-prod', |
| 35 | + 'user-auth-handler', |
| 36 | + 'payment-gateway-v2', |
| 37 | + 'notification-sender', |
| 38 | + 'data-aggregator', |
| 39 | + 'api-gateway-router', |
| 40 | + 'cache-invalidator', |
| 41 | + 'metric-collector', |
| 42 | +]; |
| 43 | + |
| 44 | +const MESSAGES = [ |
| 45 | + 'Request processed successfully', |
| 46 | + 'Lambda execution started', |
| 47 | + 'Database connection established', |
| 48 | + 'Cache hit for key', |
| 49 | + 'Retrying operation attempt', |
| 50 | + 'Invalid input parameter', |
| 51 | + 'Rate limit exceeded', |
| 52 | + 'Connection timeout', |
| 53 | + 'Memory usage threshold reached', |
| 54 | + 'Authentication token validated', |
| 55 | +]; |
| 56 | + |
| 57 | +interface LogEntry { |
| 58 | + id: string; |
| 59 | + timestamp: string; |
| 60 | + requestId: string; |
| 61 | + level: LogLevel; |
| 62 | + functionName: string; |
| 63 | + message: string; |
| 64 | + duration: number; |
| 65 | + billedDuration: number; |
| 66 | + memoryUsed: number; |
| 67 | + memoryAllocated: number; |
| 68 | +} |
| 69 | + |
| 70 | +const generateLogEntries = (count: number): LogEntry[] => { |
| 71 | + const baseTime = new Date('2026-09-27T10:00:00.000Z').getTime(); |
| 72 | + return Array.from({ length: count }, (_, i) => { |
| 73 | + const offset = i * 127; // stagger timestamps |
| 74 | + const level = LOG_LEVELS[i % LOG_LEVELS.length]; |
| 75 | + return { |
| 76 | + id: `log-${i}`, |
| 77 | + timestamp: new Date(baseTime + offset).toISOString(), |
| 78 | + requestId: `${crypto.randomUUID().slice(0, 8)}-${String(i).padStart(4, '0')}`, |
| 79 | + level, |
| 80 | + functionName: LAMBDA_FUNCTIONS[i % LAMBDA_FUNCTIONS.length], |
| 81 | + message: MESSAGES[i % MESSAGES.length], |
| 82 | + duration: Math.floor(Math.random() * 2000) + 50, |
| 83 | + billedDuration: Math.ceil((Math.floor(Math.random() * 2000) + 50) / 100) * 100, |
| 84 | + memoryUsed: Math.floor(Math.random() * 200) + 64, |
| 85 | + memoryAllocated: 256, |
| 86 | + }; |
| 87 | + }); |
| 88 | +}; |
| 89 | + |
| 90 | +const GRID_COLUMNS: ReadonlyArray<TableRootProps.ColumnDefinition> = [ |
| 91 | + { size: 40 }, // selection checkbox |
| 92 | + { size: 220 }, // timestamp |
| 93 | + { size: 140 }, // requestId |
| 94 | + { size: 70 }, // level |
| 95 | + { size: 180 }, // functionName |
| 96 | + { size: 280 }, // message |
| 97 | + { size: 100 }, // duration |
| 98 | + { size: 120 }, // billedDuration |
| 99 | + { size: 100 }, // memoryUsed |
| 100 | + { size: 120 }, // memoryAllocated |
| 101 | +]; |
| 102 | + |
| 103 | +const TOTAL_COLUMNS = GRID_COLUMNS.length; |
| 104 | +void TOTAL_COLUMNS; // Column count is used for documentation/validation |
| 105 | + |
| 106 | +function LevelIndicator({ level }: { level: LogLevel }) { |
| 107 | + const typeMap: Record<LogLevel, 'success' | 'warning' | 'error' | 'info'> = { |
| 108 | + INFO: 'info', |
| 109 | + WARN: 'warning', |
| 110 | + ERROR: 'error', |
| 111 | + DEBUG: 'success', |
| 112 | + }; |
| 113 | + return <StatusIndicator type={typeMap[level]}>{level}</StatusIndicator>; |
| 114 | +} |
| 115 | + |
| 116 | +export default function CloudWatchLogsDemo() { |
| 117 | + const [rowCount, setRowCount] = useState<'100' | '500' | '1000'>('500'); |
| 118 | + const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); |
| 119 | + |
| 120 | + const logs = useMemo(() => generateLogEntries(parseInt(rowCount, 10)), [rowCount]); |
| 121 | + |
| 122 | + const allSelected = logs.length > 0 && selectedIds.size === logs.length; |
| 123 | + const someSelected = selectedIds.size > 0 && selectedIds.size < logs.length; |
| 124 | + |
| 125 | + const handleSelectAll = () => { |
| 126 | + if (allSelected) { |
| 127 | + setSelectedIds(new Set()); |
| 128 | + } else { |
| 129 | + setSelectedIds(new Set(logs.map(l => l.id))); |
| 130 | + } |
| 131 | + }; |
| 132 | + |
| 133 | + const handleSelectRow = (id: string, checked: boolean) => { |
| 134 | + setSelectedIds(prev => { |
| 135 | + const next = new Set(prev); |
| 136 | + if (checked) { |
| 137 | + next.add(id); |
| 138 | + } else { |
| 139 | + next.delete(id); |
| 140 | + } |
| 141 | + return next; |
| 142 | + }); |
| 143 | + }; |
| 144 | + |
| 145 | + const columnLayout: TableRootProps.ColumnLayout = { type: 'grid', columns: GRID_COLUMNS }; |
| 146 | + |
| 147 | + return ( |
| 148 | + <Box padding="l"> |
| 149 | + <SpaceBetween size="l"> |
| 150 | + <Header variant="h1">CloudWatch Logs Insights — R1 Atomic Table Demo</Header> |
| 151 | + <Container |
| 152 | + header={ |
| 153 | + <Header |
| 154 | + description="Exercises sticky header, grid layout, row selection, and keyboard-accessible horizontal scroll" |
| 155 | + counter={`(${logs.length} log entries, ${selectedIds.size} selected)`} |
| 156 | + > |
| 157 | + Query results |
| 158 | + </Header> |
| 159 | + } |
| 160 | + > |
| 161 | + <SpaceBetween size="m"> |
| 162 | + <SegmentedControl |
| 163 | + selectedId={rowCount} |
| 164 | + onChange={({ detail }) => { |
| 165 | + setRowCount(detail.selectedId as '100' | '500' | '1000'); |
| 166 | + setSelectedIds(new Set()); |
| 167 | + }} |
| 168 | + label="Row count" |
| 169 | + options={[ |
| 170 | + { id: '100', text: '100 rows' }, |
| 171 | + { id: '500', text: '500 rows' }, |
| 172 | + { id: '1000', text: '1000 rows' }, |
| 173 | + ]} |
| 174 | + /> |
| 175 | + <Box color="text-body-secondary" fontSize="body-s"> |
| 176 | + Scroll down — header pins to page top. Scroll right — pinned header tracks columns. Tab to the table |
| 177 | + region and use arrow keys to scroll horizontally. |
| 178 | + </Box> |
| 179 | + </SpaceBetween> |
| 180 | + </Container> |
| 181 | + |
| 182 | + <TableRoot |
| 183 | + columnLayout={columnLayout} |
| 184 | + ariaLabel="CloudWatch Logs query results" |
| 185 | + ariaRowcount={logs.length + 1} |
| 186 | + stickyHeader={ |
| 187 | + <TableHead> |
| 188 | + <TableRow> |
| 189 | + <TableHeaderCell disablePaddings={true}> |
| 190 | + <div className={styles['selection-cell']}> |
| 191 | + <Checkbox |
| 192 | + checked={allSelected} |
| 193 | + indeterminate={someSelected} |
| 194 | + onChange={() => handleSelectAll()} |
| 195 | + ariaLabel="Select all log entries" |
| 196 | + /> |
| 197 | + </div> |
| 198 | + </TableHeaderCell> |
| 199 | + <TableHeaderCell>@timestamp</TableHeaderCell> |
| 200 | + <TableHeaderCell>@requestId</TableHeaderCell> |
| 201 | + <TableHeaderCell>@level</TableHeaderCell> |
| 202 | + <TableHeaderCell>@functionName</TableHeaderCell> |
| 203 | + <TableHeaderCell>@message</TableHeaderCell> |
| 204 | + <TableHeaderCell>@duration (ms)</TableHeaderCell> |
| 205 | + <TableHeaderCell>@billedDuration</TableHeaderCell> |
| 206 | + <TableHeaderCell>@memoryUsed (MB)</TableHeaderCell> |
| 207 | + <TableHeaderCell>@memoryAllocated</TableHeaderCell> |
| 208 | + </TableRow> |
| 209 | + </TableHead> |
| 210 | + } |
| 211 | + stickyHeaderOffset={0} |
| 212 | + > |
| 213 | + <TableBody> |
| 214 | + {logs.map((log, index) => { |
| 215 | + const isSelected = selectedIds.has(log.id); |
| 216 | + return ( |
| 217 | + <TableRow key={log.id} selected={isSelected} ariaRowindex={index + 2}> |
| 218 | + <TableBodyCell disablePaddings={true}> |
| 219 | + <div className={styles['selection-cell']}> |
| 220 | + <Checkbox |
| 221 | + checked={isSelected} |
| 222 | + onChange={({ detail }) => handleSelectRow(log.id, detail.checked)} |
| 223 | + ariaLabel={`Select log entry ${log.requestId}`} |
| 224 | + /> |
| 225 | + </div> |
| 226 | + </TableBodyCell> |
| 227 | + <TableBodyCell> |
| 228 | + <Box variant="code" fontSize="body-s"> |
| 229 | + {log.timestamp} |
| 230 | + </Box> |
| 231 | + </TableBodyCell> |
| 232 | + <TableBodyCell> |
| 233 | + <Box variant="code" fontSize="body-s"> |
| 234 | + {log.requestId} |
| 235 | + </Box> |
| 236 | + </TableBodyCell> |
| 237 | + <TableBodyCell> |
| 238 | + <LevelIndicator level={log.level} /> |
| 239 | + </TableBodyCell> |
| 240 | + <TableBodyCell> |
| 241 | + <Box variant="code" fontSize="body-s"> |
| 242 | + {log.functionName} |
| 243 | + </Box> |
| 244 | + </TableBodyCell> |
| 245 | + <TableBodyCell>{log.message}</TableBodyCell> |
| 246 | + <TableBodyCell> |
| 247 | + <Box textAlign="right">{log.duration.toLocaleString()}</Box> |
| 248 | + </TableBodyCell> |
| 249 | + <TableBodyCell> |
| 250 | + <Box textAlign="right">{log.billedDuration.toLocaleString()} ms</Box> |
| 251 | + </TableBodyCell> |
| 252 | + <TableBodyCell> |
| 253 | + <Box textAlign="right">{log.memoryUsed}</Box> |
| 254 | + </TableBodyCell> |
| 255 | + <TableBodyCell> |
| 256 | + <Box textAlign="right">{log.memoryAllocated} MB</Box> |
| 257 | + </TableBodyCell> |
| 258 | + </TableRow> |
| 259 | + ); |
| 260 | + })} |
| 261 | + </TableBody> |
| 262 | + </TableRoot> |
| 263 | + </SpaceBetween> |
| 264 | + </Box> |
| 265 | + ); |
| 266 | +} |
0 commit comments