diff --git a/src/components/replicator-coverage/ReplicatorCoverage.tsx b/src/components/replicator-coverage/ReplicatorCoverage.tsx index 63cc2d41..fb92ccbb 100644 --- a/src/components/replicator-coverage/ReplicatorCoverage.tsx +++ b/src/components/replicator-coverage/ReplicatorCoverage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import data from '@/data/replicator/coverage.json'; import { Table, @@ -8,251 +8,484 @@ import { TableHead, TableCell, } from '@/components/ui/table'; -import { - useReactTable, - getCoreRowModel, - flexRender, -} from '@tanstack/react-table'; -import type { ColumnDef, ColumnSizingState } from '@tanstack/react-table'; -import { useTableColumnSizing } from '@/hooks/useTableColumnSizing'; -import { useState } from 'react'; - -const coverage = Object.values(data); - -const columns: ColumnDef[] = [ - { - accessorKey: 'resource_type', - header: () => 'Resource Type', - cell: ({ row }) => row.original.resource_type, - size: 150, - minSize: 120, - maxSize: 200, - }, - { - accessorKey: 'service', - header: () => 'Service', - cell: ({ row }) => row.original.service, - size: 120, - minSize: 100, - maxSize: 150, - }, - { - accessorKey: 'identifier', - header: () => 'Identifier', - cell: ({ row }) => row.original.single.identifier, - size: 150, - minSize: 120, - maxSize: 200, - }, - { - accessorKey: 'policy_statements', - header: () => 'Required Actions', - cell: ({ row }) => ( - <> - {row.original.single.policy_statements.map((s: string, i: number) => ( -
{s}
- ))} - - ), - size: 300, - minSize: 200, - maxSize: 500, - }, - { - id: 'arn_support', - header: () => 'Arn Support', - cell: () => '✔️', - size: 100, - minSize: 80, - maxSize: 120, - }, -]; -export default function ReplicatorCoverage() { - // Use the reusable hook for column sizing - const { columnSizing, setColumnSizing } = useTableColumnSizing(columns); - - const table = useReactTable({ - data: coverage, - columns, - state: { - columnSizing, - }, - onColumnSizingChange: setColumnSizing, - columnResizeMode: 'onChange', - getCoreRowModel: getCoreRowModel(), - debugTable: false, - }); - - // For testing purposes, we can log the column sizing state - // console.log('Column sizing state:', columnSizing); - - // Add CSS for resizer - const resizerStyle = ` - .resizer { - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 5px; - background: rgba(0, 0, 0, 0.1); - cursor: col-resize; - user-select: none; - touch-action: none; - } - .resizer.isResizing { - background: rgba(0, 0, 0, 0.2); - opacity: 1; - } - @media (hover: hover) { - .resizer { - opacity: 0; - } - *:hover > .resizer { - opacity: 1; - } - } - `; +interface StrategyDetail { + policy_statements: string[]; + identifier: string | null; +} + +interface ResourceTree { + resources: string[]; + extra_policy_statements: string[]; +} + +interface ExtraConfigField { + type: string; + default: string; + description: string; +} + +interface Resource { + resource_type: string; + service: string; + single?: StrategyDetail; + batch?: StrategyDetail; + resource_tree?: ResourceTree; + extra_config?: Record; +} + +const coverage = data as Resource[]; + +type StrategyKind = 'single' | 'batch' | 'tree'; + +const STRATEGY_STYLES: Record< + StrategyKind, + { background: string; color: string; } +> = { + single: {background: '#e1e3eb', color: '#3a3c47'}, + batch: {background: '#afbcfa', color: '#1e40af'}, + tree: {background: '#c8aefd', color: '#3b05a7'}, +}; + +const STRATEGY_DESCRIPTIONS: Record = { + single: 'Replicates one specific resource identified by its ID.', + batch: + 'Replicates multiple resources in one job (e.g. all SSM parameters under a path).', + tree: 'Replicates this resource together with its dependent child resources.', +}; + +const legendStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '0.5rem', + flexWrap: 'wrap', +} + +const legendTitleStyle: React.CSSProperties = { + color: "var(--gray-neutral-400)", + fontFamily: "var(--font-aeonik-fono)", + fontSize: "14px", +} + +const legendItemsStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'flex-start', + gap: '1rem', + paddingLeft: '0.5rem', + flexWrap: 'wrap', + color: "var(--gray-neutral-400)", + fontSize: "14px", +}; + +const legendItemStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', +}; + +const pillStyle: React.CSSProperties = { + padding: '2px 8px', + border: '1px dashed #999CAD', + borderRadius: '4px', + fontFamily: 'var(--font-aeonik-fono)', + fontSize: '12px', + fontWeight: 500, + whiteSpace: 'nowrap', +}; + +const badgeStyle = (kind: StrategyKind): React.CSSProperties => { + const c = STRATEGY_STYLES[kind]; + return { + ...pillStyle, + background: c.background, + color: c.color, + border: `1px solid`, + }; +}; + +const configPillStyle: React.CSSProperties = { + ...pillStyle, + border: '1px dashed #999CAD', + opacity: 0.85, +}; + +const codeChipStyle: React.CSSProperties = { + fontFamily: 'var(--font-aeonik-fono)', + fontSize: '12.5px', +}; + +const cardStyle = (kind: StrategyKind): React.CSSProperties => ({ + border: `1px solid ${STRATEGY_STYLES[kind].background}`, + borderRadius: '6px', + margin: 0 +}); + +const cardHeaderStyle = (kind: StrategyKind): React.CSSProperties => { + const c = STRATEGY_STYLES[kind]; + return { + background: c.background, + color: c.color, + padding: '6px 12px', + fontWeight: 600, + fontSize: '13px', + textTransform: 'uppercase', + letterSpacing: '0.5px', + }; +}; + +const cardBodyStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: '10px', + padding: '10px 12px', +}; + +const cardDescriptionStyle: React.CSSProperties = { + opacity: 0.75, + fontSize: '12px', +}; + +const cardLabelStyle: React.CSSProperties = { + marginBottom: '3px', + fontWeight: 600, +}; + +const listStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: '3px', + margin: 0, + paddingLeft: 0, + listStyle: 'none', +}; + +const emptyStyle: React.CSSProperties = {opacity: 0.6}; + +const rowToggleStyle = (isOpen: boolean): React.CSSProperties => ({ + display: "inline-block", + flexShrink: 0, + opacity: 0.7, + marginRight: '4px', + transition: 'transform 0.15s', + transform: isOpen ? 'rotate(90deg)' : 'none', +}); + +const expandedCellStyle: React.CSSProperties = { + padding: '8px', + border: '1px solid #999CAD', + background: 'var(--sl-color-gray-6)', +}; + +const strategiesGridStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))', + gap: '12px', + alignItems: 'stretch', + width: '100%', + minWidth: 0, +}; +const extraConfigStyle: React.CSSProperties = { + marginTop: '12px', + padding: '12px', + border: '1px solid #999CAD', + borderRadius: '6px', + background: 'var(--sl-color-gray-5)', + color: 'var(--sl-color-gray-1)', + textAlign: 'left', +}; + +const extraConfigTitleStyle: React.CSSProperties = { + marginBottom: '8px', + fontWeight: 600, + fontSize: '13px', + textTransform: 'uppercase', + letterSpacing: '0.5px', +}; + +const extraConfigFieldStyle: React.CSSProperties = { + marginBottom: '8px', + fontSize: '13px', +}; + +const extraConfigMetaStyle: React.CSSProperties = {opacity: 0.7}; + +const extraConfigDescriptionStyle: React.CSSProperties = { + marginTop: '4px', + opacity: 0.9, +}; + +const tableStyle: React.CSSProperties = { + borderCollapse: 'collapse', + tableLayout: 'auto', + width: '100%', + display: 'table', +} + +const headerStyle: React.CSSProperties = { + textAlign: 'center', + border: '1px solid #999CAD', + background: 'var(--sl-color-gray-5)', + color: 'var(--sl-color-gray-1)', + fontFamily: 'var(--font-aeonik-fono)', + fontSize: '14px', + fontWeight: 500, + lineHeight: '16px', + letterSpacing: '-0.15px', + padding: '12px 8px', +}; + +const cellStyle: React.CSSProperties = { + textAlign: 'center', + border: '1px solid #999CAD', + padding: '12px 8px', + whiteSpace: 'nowrap', +}; + +const badgesContainerStyle: React.CSSProperties = { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'center', + alignItems: 'center', + gap: '4px', +}; + +const bodyStyle: React.CSSProperties = { + color: 'var(--sl-color-gray-1)', + fontSize: '14px', + fontWeight: 400, + lineHeight: '16px', +}; + +const buttonStyle: React.CSSProperties = { + fontSize: '14px', + fontWeight: 500, +}; + +function StrategyBadge({kind, label}: { kind: StrategyKind; label: string }) { + return {label}; +} + +function CodeChip({children}: { children: React.ReactNode }) { + return {children}; +} + +function StrategyCard({ + kind, + label, + identifier, + identifiers, + identifierLabel, + actions, + actionsLabel, + emptyActionsLabel, + }: { + kind: StrategyKind; + label: string; + identifier?: string | null; + identifiers?: string[] | null; + identifierLabel?: string; + actions: string[]; + actionsLabel?: string; + emptyActionsLabel?: string; +}) { return ( -
- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const meta = header.column.columnDef.meta as - | { className?: string } - | undefined; - - const getColumnWidth = (columnId: string) => { - switch (columnId) { - case 'resource_type': - return '20%'; - case 'service': - return '15%'; - case 'identifier': - return '20%'; - case 'policy_statements': - return '35%'; - case 'arn_support': - return '10%'; - default: - return 'auto'; - } - }; - - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - {header.column.getCanResize() && ( -
- )} -
- ); - })} -
- ))} -
- - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const meta = cell.column.columnDef.meta as - | { className?: string } - | undefined; - - const getColumnWidth = (columnId: string) => { - switch (columnId) { - case 'resource_type': - return '20%'; - case 'service': - return '15%'; - case 'identifier': - return '20%'; - case 'policy_statements': - return '35%'; - case 'arn_support': - return '10%'; - default: - return 'auto'; - } - }; - - return ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ); - })} - - ))} - -
+
+
{label}
+
+
{STRATEGY_DESCRIPTIONS[kind]}
+
+
{identifierLabel ?? 'Identifier'}
+ {identifier ? ( + {identifier} + ) : identifiers ? ( +
    + {identifiers.map((iden) => ( +
  • + {iden} +
  • + ))} +
+ ) : ( + None required + )} +
+
+
+ {actionsLabel ?? 'Required IAM Actions'} +
+ {actions.length === 0 ? ( + {emptyActionsLabel ?? 'None'} + ) : ( +
    + {actions.map((a) => ( +
  • + {a} +
  • + ))} +
+ )} +
); } +function ResourceRow({isOpen, row, toggle}: { isOpen: boolean, row: Resource, toggle: () => void }) { + return ( + + + + {row.resource_type} + + {row.service} + +
+ {row.single && } + {row.batch && } + {row.resource_tree && } + {row.extra_config && (+ config)} +
+
+
+ ) +} + +function ExpandedRow({row}: { row: Resource }) { + return ( + + +
+ {row.single && ( + + )} + {row.batch && ( + + )} + {row.resource_tree && ( + 0 + ? row.resource_tree!.resources + : null + } + actions={ + row.resource_tree!.extra_policy_statements + } + actionsLabel="Extra IAM Actions (in addition to Single/Batch)" + emptyActionsLabel="No extra actions required" + /> + )} +
+ {row.extra_config && ( +
+
Extra Configuration
+ {Object.entries(row.extra_config).map( + ([name, field]) => ( +
+
+ {name} + + ( {field.type}{field.default && ( + , default: {field.default}) + ) || ' )'} + +
+
{field.description}
+
+ ) + )} +
+ )} +
+
+ ) +} + +export default function ReplicatorCoverage() { + const [expanded, setExpanded] = useState>(new Set()); + + const toggle = (key: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + const expandAll = () => + setExpanded(new Set(coverage.map((r) => r.resource_type))); + const collapseAll = () => setExpanded(new Set()); + + const allOpen = expanded.size === coverage.length; + + return ( + + +
+ Legend: +
+ + + One resource + + + + Many similar resources in one job + + + + Tree of related resources + +
+
+ + + + Resource Type + Service + Replication Strategies + + + + {coverage.map((row) => { + const key = row.resource_type; + const isOpen = expanded.has(key); + return ( + + toggle(key)}/> + {isOpen && } + + ); + })} + +
+
+ ); +} + // Testing instructions: // 1. Verify that the table expands to 100% width of its container // 2. Check that columns maintain their widths during pagination diff --git a/src/content/docs/aws/tooling/aws-replicator.mdx b/src/content/docs/aws/tooling/aws-replicator.mdx index ab685081..62c332a4 100644 --- a/src/content/docs/aws/tooling/aws-replicator.mdx +++ b/src/content/docs/aws/tooling/aws-replicator.mdx @@ -398,4 +398,4 @@ For any other requests or reports, please open a [new github issue](https://gith To ensure support for all resources, use the latest LocalStack Docker image. ::: - +