diff --git a/src/components/replicator-coverage/ReplicatorCoverage.tsx b/src/components/replicator-coverage/ReplicatorCoverage.tsx index 63cc2d41..d6954cd3 100644 --- a/src/components/replicator-coverage/ReplicatorCoverage.tsx +++ b/src/components/replicator-coverage/ReplicatorCoverage.tsx @@ -1,261 +1,423 @@ -import React from 'react'; +import React, { useState } from 'react'; import data from '@/data/replicator/coverage.json'; -import { - Table, - TableHeader, - TableBody, - TableRow, - 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'; +import { ChevronRight } from 'lucide-react'; -const coverage = Object.values(data); +type ReplicationTypeInfo = { + policy_statements: string[]; + identifier: string | null; +}; -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, +type ResourceTreeInfo = { + resources: string[]; + extra_policy_statements: string[]; +}; + +type ExtraConfigField = { + type: string; + default: unknown; + description: string; +}; + +type ResourceCoverage = { + resource_type: string; + service: string; + single?: ReplicationTypeInfo; + batch?: ReplicationTypeInfo; + resource_tree?: ResourceTreeInfo; + extra_config?: Record; +}; + +const coverage = data as ResourceCoverage[]; + +type StrategyKey = 'single' | 'batch' | 'tree'; + +const STRATEGY_META: Record< + StrategyKey, + { label: string; color: string; description: string } +> = { + single: { + label: 'Single', + color: '#2563eb', + description: 'Replicate one resource at a time by identifier or ARN.', }, - { - 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, + batch: { + label: 'Batch', + color: '#7c3aed', + description: 'Discover and replicate many matching resources in one job.', }, - { - id: 'arn_support', - header: () => 'Arn Support', - cell: () => '✔️', - size: 100, - minSize: 80, - maxSize: 120, + tree: { + label: 'Tree', + color: '#0d9488', + description: + 'Use the TREE explore strategy to also replicate related child resources.', }, -]; +}; + +function StrategyBadge({ + strategy, + active, +}: { + strategy: StrategyKey; + active: boolean; +}) { + const meta = STRATEGY_META[strategy]; + return ( + + {meta.label} + + ); +} + +function PolicyList({ statements }: { statements: string[] }) { + if (!statements.length) { + return ( + + No additional actions required + + ); + } + return ( +
+ {statements.map((statement) => ( + + {statement} + + ))} +
+ ); +} + +function Identifier({ value }: { value: string | null }) { + if (!value) { + return ( + None required + ); + } + return ( + + {value} + + ); +} + +function DetailSection({ + strategy, + children, +}: { + strategy: StrategyKey; + children: React.ReactNode; +}) { + const meta = STRATEGY_META[strategy]; + return ( +
+
+ + + {meta.description} + +
+ {children} +
+ ); +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
+ {label} +
+ {children} +
+ ); +} + +function ResourceDetails({ resource }: { resource: ResourceCoverage }) { + return ( +
+ {resource.single && ( + + + + + + + + + )} + + {resource.batch && ( + + + + + + + + + )} + + {resource.resource_tree && ( + + +
+ {resource.resource_tree.resources.map((r) => ( + + {r} + + ))} +
+
+ + + +
+ )} + + {resource.extra_config && ( + + +
+ {Object.entries(resource.extra_config).map(([name, field]) => ( +
+ + {name} + {' '} + + ({field.type} + {field.default !== null && field.default !== undefined + ? `, default: ${JSON.stringify(field.default)}` + : ''} + ) + +
{field.description}
+
+ ))} +
+
+
+ )} +
+ ); +} export default function ReplicatorCoverage() { - // Use the reusable hook for column sizing - const { columnSizing, setColumnSizing } = useTableColumnSizing(columns); + const [expanded, setExpanded] = useState>({}); - const table = useReactTable({ - data: coverage, - columns, - state: { - columnSizing, - }, - onColumnSizingChange: setColumnSizing, - columnResizeMode: 'onChange', - getCoreRowModel: getCoreRowModel(), - debugTable: false, - }); + const toggle = (resourceType: string) => + setExpanded((prev) => ({ ...prev, [resourceType]: !prev[resourceType] })); - // For testing purposes, we can log the column sizing state - // console.log('Column sizing state:', columnSizing); + const cellStyle: React.CSSProperties = { + border: '1px solid var(--sl-color-gray-5)', + padding: '12px', + textAlign: 'left', + verticalAlign: 'middle', + fontFamily: 'var(--font-aeonik-fono)', + fontSize: '14px', + }; - // 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; - } - } - `; + const headStyle: React.CSSProperties = { + border: '1px solid var(--sl-color-gray-5)', + background: 'var(--sl-color-gray-6)', + color: 'var(--sl-color-white)', + fontFamily: 'var(--font-aeonik-fono)', + fontSize: '14px', + fontWeight: 600, + padding: '12px', + textAlign: 'left', + }; return ( -
- -
- +
+ {(Object.keys(STRATEGY_META) as StrategyKey[]).map((key) => ( + + + {STRATEGY_META[key].description} + + ))} +
+ +
+
- - {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() - )} - - ); - })} - - ))} - -
+ + + + + + + + + + Resource Type + Service + Replication Strategies + + + + {coverage.map((resource) => { + const isOpen = !!expanded[resource.resource_type]; + return ( + + toggle(resource.resource_type)} + style={{ cursor: 'pointer' }} + > + + + + + + {resource.resource_type} + + + {resource.service} + +
+ + + +
+ + + {isOpen && ( + + + + + + )} +
+ ); + })} + +
); } - -// Testing instructions: -// 1. Verify that the table expands to 100% width of its container -// 2. Check that columns maintain their widths during pagination -// 3. Test with different viewport sizes to ensure responsive behavior -// 4. Try resizing columns to ensure the resize functionality works -// 5. Verify that content in cells is properly displayed with ellipsis for overflow diff --git a/src/content/docs/aws/tooling/aws-replicator.mdx b/src/content/docs/aws/tooling/aws-replicator.mdx index a6738e96..f05ff9a3 100644 --- a/src/content/docs/aws/tooling/aws-replicator.mdx +++ b/src/content/docs/aws/tooling/aws-replicator.mdx @@ -70,6 +70,17 @@ Both methods have two steps: 1. Submit a replication job. 2. Check the job status. +#### Replication strategies + +The Replicator supports different strategies depending on how many resources you want to copy and whether their related resources should be included. +The [supported resources](#supported-resources) table shows which strategies are available for each resource type, along with the IAM actions required for each. + +- **Single** (`SINGLE_RESOURCE`): replicate a single resource identified by its identifier or ARN. This is the default. +- **Batch** (`BATCH`): discover and replicate every matching resource in a single job, for example all SSM parameters under a path prefix or all S3 buckets matching a prefix. +- **Tree** (`TREE` explore strategy): starting from a single resource, also replicate its related child resources. For example, replicating an `AWS::Organizations::Organization` also replicates its organizational units, accounts, and policies. + +The replication type and explore strategy are independent: `replication_type` selects between single-resource and batch discovery, while `explore_strategy` (`SIMPLE` or `TREE`) controls whether related resources are followed. + #### Using the LocalStack CLI The Replicator CLI is part of the LocalStack CLI. @@ -189,6 +200,29 @@ For example: This triggers a batch replication job that discovers and replicates all SSM parameters under the `/dev/` path prefix. +To replicate a resource together with its related resources, set `explore_strategy` to `TREE`. +For example, to replicate an entire organization tree: + +```json +{ + "replication_type": "SINGLE_RESOURCE", + "explore_strategy": "TREE", + "replication_job_config": { + "resource_type": "AWS::Organizations::Organization", + "identifier": "o-exampleorgid" + }, + "source_aws_config": { + "aws_access_key_id": "...", + "aws_secret_access_key": "...", + "region_name": "..." + } +} +``` + +When `explore_strategy` is omitted, the default is `SIMPLE`, which replicates only the requested resource. +With `TREE`, the Replicator follows the resource's relationships and replicates the related resources listed in the [supported resources](#supported-resources) table. +The additional IAM actions shown for the **Tree** strategy must also be granted. + ### Check Replication Job Status @@ -397,4 +431,7 @@ Please open a [new GitHub Discussion](https://github.com/orgs/localstack/discuss To ensure support for all resources, use the latest LocalStack Docker image. ::: - +The table below lists every supported resource type and the [replication strategies](#replication-strategies) available for each. +Select a row to expand it and view the resource identifier, the IAM actions required for each strategy, and any related resources replicated by the Tree strategy. + +