@aws/nx-plugin-mcp 0.119.0 → 0.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/bin/aws-nx-mcp.js +23 -35
  2. package/docs/guides/astro-docs.mdx +142 -0
  3. package/docs/guides/connection/py-agent-a2a.mdx +105 -0
  4. package/docs/guides/connection/py-agent-mcp.mdx +147 -0
  5. package/docs/guides/connection/react-agui.mdx +240 -0
  6. package/docs/guides/connection/react-fastapi.mdx +1476 -0
  7. package/docs/guides/connection/react-py-agent.mdx +198 -0
  8. package/docs/guides/connection/react-smithy.mdx +1355 -0
  9. package/docs/guides/connection/react-trpc.mdx +447 -0
  10. package/docs/guides/connection/react-ts-agent.mdx +198 -0
  11. package/docs/guides/connection/smithy-rdb.mdx +161 -0
  12. package/docs/guides/connection/trpc-rdb.mdx +127 -0
  13. package/docs/guides/connection/ts-agent-a2a.mdx +106 -0
  14. package/docs/guides/connection/ts-agent-mcp.mdx +144 -0
  15. package/docs/guides/connection/ts-agent-rdb.mdx +141 -0
  16. package/docs/guides/connection/ts-mcp-server-rdb.mdx +135 -0
  17. package/docs/guides/connection.mdx +126 -0
  18. package/docs/guides/docker-bundling.mdx +403 -0
  19. package/docs/guides/fastapi.mdx +509 -0
  20. package/docs/guides/license.mdx +293 -0
  21. package/docs/guides/nx-generator.mdx +588 -0
  22. package/docs/guides/py-agent.mdx +483 -0
  23. package/docs/guides/py-mcp-server.mdx +161 -0
  24. package/docs/guides/python-lambda-function.mdx +207 -0
  25. package/docs/guides/python-project.mdx +228 -0
  26. package/docs/guides/react-website-auth.mdx +216 -0
  27. package/docs/guides/react-website.mdx +421 -0
  28. package/docs/guides/runtime-config.mdx +312 -0
  29. package/docs/guides/terraform-project.mdx +317 -0
  30. package/docs/guides/trpc.mdx +816 -0
  31. package/docs/guides/ts-agent.mdx +436 -0
  32. package/docs/guides/ts-lambda-function.mdx +218 -0
  33. package/docs/guides/ts-mcp-server.mdx +169 -0
  34. package/docs/guides/ts-nx-plugin.mdx +159 -0
  35. package/docs/guides/ts-rdb.mdx +759 -0
  36. package/docs/guides/ts-smithy-api.mdx +661 -0
  37. package/docs/guides/typescript-infrastructure.mdx +408 -0
  38. package/docs/guides/typescript-project.mdx +312 -0
  39. package/docs/guides/workspace.mdx +181 -0
  40. package/docs/snippets/agent/architecture.mdx +72 -0
  41. package/docs/snippets/agent/bedrock-deployment.mdx +172 -0
  42. package/docs/snippets/agent/runtime-arn.mdx +64 -0
  43. package/docs/snippets/api/api-architecture.mdx +93 -0
  44. package/docs/snippets/api/api-choice-note.mdx +6 -0
  45. package/docs/snippets/api/cors-configuration-cdk-note.mdx +25 -0
  46. package/docs/snippets/api/cors-configuration-terraform-note.mdx +28 -0
  47. package/docs/snippets/api/shared-constructs.mdx +38 -0
  48. package/docs/snippets/api/type-safe-api-integrations.mdx +643 -0
  49. package/docs/snippets/api/waf-configuration.mdx +37 -0
  50. package/docs/snippets/connection/a2a-infrastructure.mdx +63 -0
  51. package/docs/snippets/connection/lambda-rdb-ssl-requirements.mdx +40 -0
  52. package/docs/snippets/connection/mcp-server-rdb-ssl-requirements.mdx +35 -0
  53. package/docs/snippets/connection/rdb-api-infrastructure.mdx +72 -0
  54. package/docs/snippets/connection/react-agent-infrastructure.mdx +61 -0
  55. package/docs/snippets/connection/strands-agent-rdb-ssl-requirements.mdx +35 -0
  56. package/docs/snippets/lambda-function/architecture.mdx +36 -0
  57. package/docs/snippets/lambda-function/deploying-your-function.mdx +118 -0
  58. package/docs/snippets/mcp/architecture.mdx +58 -0
  59. package/docs/snippets/mcp/assistant-docs.mdx +10 -0
  60. package/docs/snippets/mcp/bedrock-deployment.mdx +167 -0
  61. package/docs/snippets/mcp/config.mdx +13 -0
  62. package/docs/snippets/mcp/configuration-py.mdx +42 -0
  63. package/docs/snippets/mcp/configuration-ts.mdx +53 -0
  64. package/docs/snippets/mcp/observability.mdx +8 -0
  65. package/docs/snippets/mcp/shared-constructs.mdx +32 -0
  66. package/docs/snippets/pdk-migration/example/01-migrate-api.mdx +602 -0
  67. package/docs/snippets/pdk-migration/example/02-migrate-website.mdx +915 -0
  68. package/docs/snippets/pdk-migration/example/03-migrate-infra.mdx +161 -0
  69. package/docs/snippets/pdk-migration/example/04-deploy.mdx +229 -0
  70. package/docs/snippets/pdk-migration/faq/aws-arch.mdx +17 -0
  71. package/docs/snippets/pdk-migration/faq/cdk-graph.mdx +29 -0
  72. package/docs/snippets/pdk-migration/faq/infrastructure-python-java.mdx +19 -0
  73. package/docs/snippets/pdk-migration/faq/pdk-nag.mdx +15 -0
  74. package/docs/snippets/pdk-migration/faq/pipeline.mdx +15 -0
  75. package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +310 -0
  76. package/docs/snippets/pdk-migration/faq/use-of-projen.mdx +15 -0
  77. package/docs/snippets/prerequisites.mdx +20 -0
  78. package/docs/snippets/required-prerequisites.mdx +12 -0
  79. package/docs/snippets/shared-constructs.mdx +40 -0
  80. package/docs/snippets/tools/acurl.mdx +73 -0
  81. package/docs/snippets/ts-bundle.mdx +14 -0
  82. package/package.json +1 -1
  83. package/src/py/agent/schema.json +1 -1
  84. package/src/py/fast-api/schema.json +2 -2
  85. package/src/py/mcp-server/schema.json +1 -1
  86. package/src/smithy/ts/api/schema.json +2 -2
  87. package/src/trpc/backend/schema.json +2 -2
  88. package/src/ts/agent/schema.json +1 -1
  89. package/src/ts/mcp-server/schema.json +1 -1
@@ -0,0 +1,915 @@
1
+ ---
2
+ title: Migrate the Website
3
+ ---
4
+ import { Steps, Tabs, TabItem } from '@astrojs/starlight/components';
5
+ import CreateNxWorkspaceCommand from '@components/create-nx-workspace-command.astro';
6
+ import RunGenerator from '@components/run-generator.astro';
7
+ import Link from '@components/link.astro';
8
+ import NxCommands from '@components/nx-commands.astro';
9
+ import Drawer from '@components/drawer.astro';
10
+ import InstallCommand from '@components/install-command.astro';
11
+
12
+ The `CloudscapeReactTsWebsiteProject` used in the shopping list application configured a React website with CloudScape and Cognito authentication built in.
13
+
14
+ This project type leveraged [`create-react-app`](https://github.com/facebook/create-react-app), which is now deprecated. For migrating the website in this guide, we will use the <Link path="/guides/react-website">`ts#react-website` generator</Link>, which uses more modern and supported technologies, namely [Vite](https://vite.dev/).
15
+
16
+ As part of the migration, we will also move from PDK's configured React Router to [TanStack Router](https://tanstack.com/router), which adds additional type-safety to website routing.
17
+
18
+ #### Generate a React Website
19
+
20
+ Run the <Link path="/guides/react-website">`ts#react-website` generator</Link> to set up your website project in `packages/website`:
21
+
22
+ <RunGenerator generator="ts#react-website" noInteractive requiredParameters={{ name: 'website' }} />
23
+
24
+ #### Add Cognito Authentication
25
+
26
+ The React website generator above doesn't bundle cognito authentication by default like `CloudscapeReactTsWebsiteProject`, instead it's added explicitly via the <Link path="/guides/react-website-auth">`ts#react-website#auth` generator</Link>.
27
+
28
+ <RunGenerator generator="ts#react-website#auth" noInteractive requiredParameters={{ project: 'website', cognitoDomain: 'shopping-list' }} />
29
+
30
+ This adds React components which manage the appropriate redirects to ensure users log in using the Cognito hosted UI. This also adds a CDK construct to deploy the Cognito resources in `packages/common/constructs`, called `UserIdentity`.
31
+
32
+ :::note
33
+ The PDK `CloudscapeReactTsWebsiteProject` relied on `@aws-northstar/ui` to provide Cognito login components in-app. If you would prefer to use this approach instead of moving to the hosted UI, you can replace the generated `packages/common/constructs/src/core/user-identity.ts` with the [implementation from PDK](https://github.com/aws/aws-pdk/blob/mainline/packages/identity/src/user-identity.ts), replacing the constructs from `@aws-cdk/aws-cognito-identitypool-alpha` with those vended by `aws-cdk-lib`, and make sure to set the relevant values in `RuntimeConfig`:
34
+
35
+ ```ts
36
+ RuntimeConfig.ensure(this).config = {
37
+ ...RuntimeConfig.ensure(this).config,
38
+ region: Stack.of(this).region,
39
+ identityPoolId: this.identityPool.identityPoolId,
40
+ userPoolId: this.userPool.userPoolId,
41
+ userPoolWebClientId: this.userPoolClient.userPoolClientId,
42
+ };
43
+ ```
44
+
45
+ You can then reuse the `Auth` component from your `CloudscapeReactTsWebsiteProject`.
46
+ :::
47
+
48
+ #### Connect the Website to the API
49
+
50
+ In PDK you could pass the vended Projen projects to one another to trigger integration code to be vended. This was used in the shopping list application to configure the website to be able to integrate with the API.
51
+
52
+ With the Nx Plugin for AWS, API integration is supported via the <Link path="/guides/connection">`connection` generator</Link>. Next, we use this generator so that our website can invoke our Smithy API:
53
+
54
+ <RunGenerator generator="connection" noInteractive requiredParameters={{ sourceProject: 'website', targetProject: 'api' }} />
55
+
56
+ This generates the necessary client providers and build targets for your website to call your API via a generated TypeScript client.
57
+
58
+ #### Add AWS Northstar Dependency
59
+
60
+ The `CloudscapeReactTsWebsiteProject` automatically included a dependency on `@aws-northstar/ui` which is used in our shopping list application, so we add it here:
61
+
62
+ <InstallCommand pkg="@aws-northstar/ui" />
63
+
64
+ #### Move the Components and Pages
65
+
66
+ The [shopping list application](https://aws.github.io/aws-pdk/getting_started/shopping_list_app.html#create-new-pages-components) has one component called `CreateItem`, and two pages, `ShoppingList` and `ShoppingLists`. We'll migrate these to the new website, making a few adjustments since we're using TanStack Router and the Nx Plugin for AWS TypeScript client code generator.
67
+
68
+ <Steps>
69
+
70
+ 1. Copy `packages/website/src/components/CreateItem/index.tsx` from the PDK project into the exact same location in the new project.
71
+
72
+ 1. Copy `packages/website/src/pages/ShoppingLists/index.tsx` to `packages/website/src/routes/index.tsx`, since `ShoppingLists` is our home page and we use file-based routing with TanStack router.
73
+
74
+ 1. Copy `packages/website/src/pages/ShoppingList/index.tsx` to `packages/website/src/routes/$shoppingListId.tsx`, since `ShoppingList` was the page we want to show on the `/:shoppingListId` route.
75
+
76
+ </Steps>
77
+
78
+ Note that you'll now have some build errors visible in your IDE, we'll need to make a few more changes to fit within the new framework, outlined below.
79
+
80
+ #### Migrate from React Router to TanStack Router
81
+
82
+ Since we're using [file-based routing](https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing), we can use the website local development server to manage automatically generating route configuration. Let's start the local website server:
83
+
84
+ <NxCommands commands={["serve-local website"]} />
85
+
86
+ :::tip
87
+ We're using the `serve-local` target here, which also starts local servers for any APIs which have been connected with `connection`, and hot-reloads if your website, model, or backend changes! This allows us to test our API and website locally before we've even written any CDK code.
88
+ :::
89
+
90
+ You'll see some errors, but the local website server should start on port `4200`, as well as the local Smithy API server on port `3001`.
91
+
92
+ Follow the below steps in both `routes/index.tsx` and `routes/$shoppingListId.tsx` to migrate to TanStack Router:
93
+
94
+ <Steps>
95
+
96
+ 1. Add `createFileRoute` to register each route:
97
+
98
+ <Tabs>
99
+ <TabItem label="routes/index.tsx">
100
+ ```diff lang="ts"
101
+ +import { createFileRoute } from "@tanstack/react-router";
102
+ ...
103
+ -export default ShoppingLists;
104
+ +export const Route = createFileRoute('/')({
105
+ + component: ShoppingLists,
106
+ +});
107
+ ```
108
+ </TabItem>
109
+ <TabItem label="routes/$shoppingListId.tsx">
110
+ ```diff lang="ts"
111
+ +import { createFileRoute } from "@tanstack/react-router";
112
+ ...
113
+ -export default ShoppingList;
114
+ +export const Route = createFileRoute('/$shoppingListId')({
115
+ + component: ShoppingList,
116
+ +});
117
+ ```
118
+ </TabItem>
119
+ </Tabs>
120
+
121
+ After you save the file, you'll notice that type errors with call to `createFileRoute` have gone.
122
+
123
+ 1. Replace the `useNavigate` hook.
124
+
125
+ Update the import:
126
+
127
+ ```diff lang="ts"
128
+ - import { useNavigate } from 'react-router-dom';
129
+ + import { useNavigate } from '@tanstack/react-router';
130
+ ```
131
+
132
+ Update calls to the `navigate` method (returned by `useNavigate`) to pass in the type-safe routes:
133
+
134
+ ```diff lang="ts"
135
+ - navigate(`/${cell.shoppingListId}`);
136
+ + navigate({
137
+ + to: '/$shoppingListId',
138
+ + params: { shoppingListId: cell.shoppingListId },
139
+ + });
140
+ ```
141
+
142
+ 1. Replace the `useParams` hook.
143
+
144
+ Remove the import:
145
+
146
+ ```diff lang="ts"
147
+ - import { useParams } from 'react-router-dom';
148
+ ```
149
+
150
+ Update calls to `useParams` with the hook provided by the `Route` created above. These are now type-safe!
151
+
152
+ ```diff lang="ts"
153
+ - const { shoppingListId } = useParams();
154
+ + const { shoppingListId } = Route.useParams();
155
+ ```
156
+
157
+ </Steps>
158
+
159
+ :::tip
160
+ You can also check out the [detailed guide in the TanStack Router documentation](https://tanstack.com/router/latest/docs/installation/migrate-from-react-router) for more in-depth information for more complex scenarios.
161
+ :::
162
+
163
+ #### Fix Component Imports
164
+
165
+ Since our route files aren't as deeply nested in the file tree as they were in our PDK project, we need to fix the import for `CreateItem` in both `routes/index.tsx` and `routes/$shoppingListId.tsx`:
166
+
167
+ ```diff lang="ts"
168
+ -import CreateItem from "../../components/CreateItem";
169
+ +import CreateItem from "../components/CreateItem";
170
+ ```
171
+
172
+ The `AppLayoutContext` is also vended in a slightly different location in our new project:
173
+
174
+ ```diff lang="ts"
175
+ -import { AppLayoutContext } from "../../layouts/App";
176
+ +import { AppLayoutContext } from "../components/AppLayout";
177
+ ```
178
+
179
+ #### Migrate to use the new Generated TypeScript Client
180
+
181
+ We're getting closer now! Next, we need to migrate to use the TypeScript client vended by the Nx Plugin for AWS, which has a few improvements compared to Type Safe API. To achieve this, follow the below steps
182
+
183
+ <Steps>
184
+
185
+ 1. Import the new generated client and types instead of the old, for example:
186
+
187
+ ```diff lang="ts"
188
+ -import {
189
+ - ShoppingList,
190
+ - usePutShoppingList,
191
+ - useDeleteShoppingList,
192
+ - useGetShoppingLists,
193
+ -} from "myapi-typescript-react-query-hooks";
194
+ +import { ShoppingList } from "../generated/my-api/types.gen";
195
+ +import { useMyApi } from "../hooks/useMyApi";
196
+ +import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
197
+ ```
198
+
199
+ Note that `routes/$shoppingListId.tsx` imports the `ShoppingList` type as `_ShoppingList` - in that file we should do the same, but again importing from `types.gen`.
200
+
201
+ Note also we import the relevant hooks directly from `@tanstack/react-query`, since the generated client provides methods to generate options for TanStack query hooks, rather than hook wrappers.
202
+
203
+ 1. Instantiate the new TanStack Query hooks, for example:
204
+
205
+ ```diff lang="ts"
206
+ -const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });
207
+ -const putShoppingList = usePutShoppingList();
208
+ -const deleteShoppingList = useDeleteShoppingList();
209
+ +const api = useMyApi();
210
+ +const getShoppingLists = useInfiniteQuery(
211
+ + api.getShoppingLists.infiniteQueryOptions(
212
+ + { pageSize: PAGE_SIZE },
213
+ + { getNextPageParam: (p) => p.nextToken },
214
+ + ),
215
+ +);
216
+ +const putShoppingList = useMutation(api.putShoppingList.mutationOptions());
217
+ +const deleteShoppingList = useMutation(
218
+ + api.deleteShoppingList.mutationOptions(),
219
+ +);
220
+ ```
221
+
222
+ 1. Remove the wrapper `<operation>RequestContent` for calls to operations which accept parameters in the request body:
223
+
224
+ ```diff lang="ts"
225
+ await putShoppingList.mutateAsync({
226
+ - putShoppingListRequestContent: {
227
+ name: item,
228
+ - },
229
+ });
230
+ ```
231
+
232
+ </Steps>
233
+
234
+ :::note
235
+ There was also a minor bug in the shopping list application's `pages/ShoppingList/index.tsx` where it was not using optional chaining correctly. Let's fix this too to avoid a lint error when we run our final build:
236
+
237
+ ```diff lang="ts"
238
+ const shoppingList: _ShoppingList | undefined =
239
+ - getShoppingLists.data?.pages[0].shoppingLists[0]!;
240
+ + getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0];
241
+ ```
242
+
243
+ And likewise update the references which assumed it's defined:
244
+
245
+ ```diff lang="ts"
246
+ -name: shoppingList.name,
247
+ -shoppingListId: shoppingList.shoppingListId,
248
+ +name: shoppingList?.name ?? 'my list',
249
+ +shoppingListId: shoppingList?.shoppingListId,
250
+ ```
251
+ :::
252
+
253
+ #### Migrate from TanStack Query v4 to v5
254
+
255
+ There are a few errors left to fix due to differences between TanStack Query v4 (used by PDK) and v5 which the `connection` generator added:
256
+
257
+ <Steps>
258
+
259
+ 1. Replace `isLoading` with `isPending` for mutations, for example:
260
+
261
+ ```diff lang="ts"
262
+ -putShoppingList.isLoading
263
+ +putShoppingList.isPending
264
+ ```
265
+
266
+ 1. The shopping list application made use of the `InfiniteQueryTable` from `@aws-northstar/ui` which expects a type from TanStack Query v4. This actually works with infinite queries from v5, so we can just suppress the type error:
267
+
268
+ ```diff lang="tsx"
269
+ <InfiniteQueryTable
270
+ - query={getShoppingLists}
271
+ + query={getShoppingLists as any}
272
+ ```
273
+
274
+ </Steps>
275
+
276
+ :::tip
277
+ The TanStack Query documentation has a [detailed migration guide](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5) which you can follow if you have used more features of the library.
278
+ :::
279
+
280
+ #### Visit the Local Website
281
+
282
+ You can now visit the local website at http://localhost:4200/
283
+
284
+ The website should load up now that everything's been migrated! Since the only infrastructure that the shopping list application relies on besides API, Website and Identity is the DynamoDB table - if you've got a DynamoDB table named `shopping_list` in-region, and local AWS credentials which can access it, the website will be fully functional!
285
+
286
+ If not, that's ok, we'll migrate the infrastructure next.
287
+
288
+ <Drawer title="Shopping List Page Migration" trigger="Click here for full before/after examples for the two shopping list pages from the tutorial">
289
+
290
+ <h4>Shopping Lists Page</h4>
291
+
292
+ <Tabs syncKey="pdk-migration">
293
+ <TabItem label="Before">
294
+ ```tsx
295
+ // pages/ShoppingLists/index.tsx
296
+ /* eslint-disable @typescript-eslint/no-floating-promises */
297
+ import { InfiniteQueryTable } from "@aws-northstar/ui/components";
298
+ import {
299
+ Button,
300
+ Header,
301
+ Link,
302
+ SpaceBetween,
303
+ TableProps,
304
+ } from "@cloudscape-design/components";
305
+ import {
306
+ ShoppingList,
307
+ usePutShoppingList,
308
+ useDeleteShoppingList,
309
+ useGetShoppingLists,
310
+ } from "myapi-typescript-react-query-hooks";
311
+ import { useContext, useEffect, useMemo, useState } from "react";
312
+ import { useNavigate } from "react-router-dom";
313
+ import CreateItem from "../../components/CreateItem";
314
+ import { AppLayoutContext } from "../../layouts/App";
315
+
316
+ const PAGE_SIZE = 50;
317
+
318
+ /**
319
+ * Component to render the ShoppingLists "/" route.
320
+ */
321
+ const ShoppingLists: React.FC = () => {
322
+ const [visibleModal, setVisibleModal] = useState(false);
323
+ const [selectedShoppingList, setSelectedShoppingList] = useState<
324
+ ShoppingList[]
325
+ >([]);
326
+ const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });
327
+ const putShoppingList = usePutShoppingList();
328
+ const deleteShoppingList = useDeleteShoppingList();
329
+ const navigate = useNavigate();
330
+ const { setAppLayoutProps } = useContext(AppLayoutContext);
331
+
332
+ useEffect(() => {
333
+ setAppLayoutProps({
334
+ contentType: "table",
335
+ });
336
+ }, [setAppLayoutProps]);
337
+
338
+ const columnDefinitions = useMemo<
339
+ TableProps.ColumnDefinition<ShoppingList>[]
340
+ >(
341
+ () => [
342
+ {
343
+ id: "shoppingListId",
344
+ isRowHeader: true,
345
+ header: "Shopping List Id",
346
+ cell: (cell) => (
347
+ <Link
348
+ href={`/${cell.shoppingListId}`}
349
+ onFollow={(e) => {
350
+ e.preventDefault();
351
+ navigate(`/${cell.shoppingListId}`);
352
+ }}
353
+ >
354
+ {cell.shoppingListId}
355
+ </Link>
356
+ ),
357
+ },
358
+ {
359
+ id: "name",
360
+ header: "Name",
361
+ cell: (cell) => cell.name,
362
+ },
363
+ {
364
+ id: "shoppingItems",
365
+ header: "Shopping Items",
366
+ cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`,
367
+ },
368
+ ],
369
+ [navigate],
370
+ );
371
+
372
+ return (
373
+ <>
374
+ <CreateItem
375
+ title="Create Shopping List"
376
+ callback={async (item) => {
377
+ await putShoppingList.mutateAsync({
378
+ putShoppingListRequestContent: {
379
+ name: item,
380
+ },
381
+ });
382
+ getShoppingLists.refetch();
383
+ }}
384
+ isLoading={putShoppingList.isLoading}
385
+ visibleModal={visibleModal}
386
+ setVisibleModal={setVisibleModal}
387
+ />
388
+ <InfiniteQueryTable
389
+ query={getShoppingLists}
390
+ itemsKey="shoppingLists"
391
+ pageSize={PAGE_SIZE}
392
+ selectionType="single"
393
+ stickyHeader={true}
394
+ selectedItems={selectedShoppingList}
395
+ onSelectionChange={(e) =>
396
+ setSelectedShoppingList(e.detail.selectedItems)
397
+ }
398
+ header={
399
+ <Header
400
+ variant="awsui-h1-sticky"
401
+ actions={
402
+ <SpaceBetween size="xs" direction="horizontal">
403
+ <Button
404
+ loading={deleteShoppingList.isLoading}
405
+ data-testid="header-btn-delete"
406
+ disabled={selectedShoppingList.length === 0}
407
+ onClick={async () => {
408
+ await deleteShoppingList.mutateAsync({
409
+ shoppingListId: selectedShoppingList![0].shoppingListId,
410
+ });
411
+ setSelectedShoppingList([]);
412
+ getShoppingLists.refetch();
413
+ }}
414
+ >
415
+ Delete
416
+ </Button>
417
+ <Button
418
+ data-testid="header-btn-create"
419
+ variant="primary"
420
+ onClick={() => setVisibleModal(true)}
421
+ >
422
+ Create Shopping List
423
+ </Button>
424
+ </SpaceBetween>
425
+ }
426
+ >
427
+ Shopping Lists
428
+ </Header>
429
+ }
430
+ variant="full-page"
431
+ columnDefinitions={columnDefinitions}
432
+ />
433
+ </>
434
+ );
435
+ };
436
+
437
+ export default ShoppingLists;
438
+ ```
439
+ </TabItem>
440
+ <TabItem label="After">
441
+ ```tsx
442
+ // routes/index.tsx
443
+ /* eslint-disable @typescript-eslint/no-floating-promises */
444
+ import { InfiniteQueryTable } from "@aws-northstar/ui/components";
445
+ import {
446
+ Button,
447
+ Header,
448
+ Link,
449
+ SpaceBetween,
450
+ TableProps,
451
+ } from "@cloudscape-design/components";
452
+ import { useContext, useEffect, useMemo, useState } from "react";
453
+ import { useNavigate } from "@tanstack/react-router";
454
+ import CreateItem from "../components/CreateItem";
455
+ import { AppLayoutContext } from "../components/AppLayout";
456
+ import { createFileRoute } from "@tanstack/react-router";
457
+ import { ShoppingList } from "../generated/my-api/types.gen";
458
+ import { useMyApi } from "../hooks/useMyApi";
459
+ import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
460
+
461
+ const PAGE_SIZE = 50;
462
+
463
+ /**
464
+ * Component to render the ShoppingLists "/" route.
465
+ */
466
+ const ShoppingLists: React.FC = () => {
467
+ const [visibleModal, setVisibleModal] = useState(false);
468
+ const [selectedShoppingList, setSelectedShoppingList] = useState<
469
+ ShoppingList[]
470
+ >([]);
471
+ const api = useMyApi();
472
+ const getShoppingLists = useInfiniteQuery(
473
+ api.getShoppingLists.infiniteQueryOptions(
474
+ { pageSize: PAGE_SIZE },
475
+ { getNextPageParam: (res) => res.nextToken },
476
+ ),
477
+ );
478
+ const putShoppingList = useMutation(api.putShoppingList.mutationOptions());
479
+ const deleteShoppingList = useMutation(
480
+ api.deleteShoppingList.mutationOptions(),
481
+ );
482
+ const navigate = useNavigate();
483
+ const { setAppLayoutProps } = useContext(AppLayoutContext);
484
+
485
+ useEffect(() => {
486
+ setAppLayoutProps({
487
+ contentType: "table",
488
+ });
489
+ }, [setAppLayoutProps]);
490
+
491
+ const columnDefinitions = useMemo<
492
+ TableProps.ColumnDefinition<ShoppingList>[]
493
+ >(
494
+ () => [
495
+ {
496
+ id: "shoppingListId",
497
+ isRowHeader: true,
498
+ header: "Shopping List Id",
499
+ cell: (cell) => (
500
+ <Link
501
+ href={`/${cell.shoppingListId}`}
502
+ onFollow={(e) => {
503
+ e.preventDefault();
504
+ navigate({
505
+ to: '/$shoppingListId',
506
+ params: { shoppingListId: cell.shoppingListId },
507
+ });
508
+ }}
509
+ >
510
+ {cell.shoppingListId}
511
+ </Link>
512
+ ),
513
+ },
514
+ {
515
+ id: "name",
516
+ header: "Name",
517
+ cell: (cell) => cell.name,
518
+ },
519
+ {
520
+ id: "shoppingItems",
521
+ header: "Shopping Items",
522
+ cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`,
523
+ },
524
+ ],
525
+ [navigate],
526
+ );
527
+
528
+ return (
529
+ <>
530
+ <CreateItem
531
+ title="Create Shopping List"
532
+ callback={async (item) => {
533
+ await putShoppingList.mutateAsync({
534
+ name: item,
535
+ });
536
+ getShoppingLists.refetch();
537
+ }}
538
+ isLoading={putShoppingList.isPending}
539
+ visibleModal={visibleModal}
540
+ setVisibleModal={setVisibleModal}
541
+ />
542
+ <InfiniteQueryTable
543
+ query={getShoppingLists as any}
544
+ itemsKey="shoppingLists"
545
+ pageSize={PAGE_SIZE}
546
+ selectionType="single"
547
+ stickyHeader={true}
548
+ selectedItems={selectedShoppingList}
549
+ onSelectionChange={(e) =>
550
+ setSelectedShoppingList(e.detail.selectedItems)
551
+ }
552
+ header={
553
+ <Header
554
+ variant="awsui-h1-sticky"
555
+ actions={
556
+ <SpaceBetween size="xs" direction="horizontal">
557
+ <Button
558
+ loading={deleteShoppingList.isPending}
559
+ data-testid="header-btn-delete"
560
+ disabled={selectedShoppingList.length === 0}
561
+ onClick={async () => {
562
+ await deleteShoppingList.mutateAsync({
563
+ shoppingListId: selectedShoppingList![0].shoppingListId,
564
+ });
565
+ setSelectedShoppingList([]);
566
+ getShoppingLists.refetch();
567
+ }}
568
+ >
569
+ Delete
570
+ </Button>
571
+ <Button
572
+ data-testid="header-btn-create"
573
+ variant="primary"
574
+ onClick={() => setVisibleModal(true)}
575
+ >
576
+ Create Shopping List
577
+ </Button>
578
+ </SpaceBetween>
579
+ }
580
+ >
581
+ Shopping Lists
582
+ </Header>
583
+ }
584
+ variant="full-page"
585
+ columnDefinitions={columnDefinitions}
586
+ />
587
+ </>
588
+ );
589
+ };
590
+
591
+ export const Route = createFileRoute('/')({
592
+ component: ShoppingLists,
593
+ });
594
+ ```
595
+ </TabItem>
596
+ </Tabs>
597
+
598
+ <h4>Shopping List Page</h4>
599
+
600
+ <Tabs syncKey="pdk-migration">
601
+ <TabItem label="Before">
602
+ ```tsx
603
+ // pages/ShoppingList/index.tsx
604
+ /* eslint-disable @typescript-eslint/no-floating-promises */
605
+ import {
606
+ Board,
607
+ BoardItem,
608
+ BoardProps,
609
+ } from "@cloudscape-design/board-components";
610
+ import {
611
+ Button,
612
+ Container,
613
+ ContentLayout,
614
+ Header,
615
+ SpaceBetween,
616
+ Spinner,
617
+ } from "@cloudscape-design/components";
618
+ import {
619
+ ShoppingList as _ShoppingList,
620
+ usePutShoppingList,
621
+ useGetShoppingLists,
622
+ } from "myapi-typescript-react-query-hooks";
623
+ import { useEffect, useState } from "react";
624
+ import { useParams } from "react-router-dom";
625
+ import CreateItem from "../../components/CreateItem";
626
+
627
+ type ListItem = { name: string };
628
+
629
+ /**
630
+ * Component to render a singular Shopping List "/:shoppingListId" route.
631
+ */
632
+ const ShoppingList: React.FC = () => {
633
+ const { shoppingListId } = useParams();
634
+ const [visibleModal, setVisibleModal] = useState(false);
635
+ const getShoppingLists = useGetShoppingLists({ shoppingListId });
636
+ const putShoppingList = usePutShoppingList();
637
+ const shoppingList: _ShoppingList | undefined =
638
+ getShoppingLists.data?.pages[0].shoppingLists[0]!;
639
+ const [shoppingItems, setShoppingItems] =
640
+ useState<BoardProps.Item<ListItem>[]>();
641
+
642
+ useEffect(() => {
643
+ setShoppingItems(
644
+ shoppingList?.shoppingItems?.map((i) => ({
645
+ id: i,
646
+ definition: { minColumnSpan: 4 },
647
+ data: { name: i },
648
+ })),
649
+ );
650
+ }, [shoppingList?.shoppingItems]);
651
+
652
+ return (
653
+ <ContentLayout
654
+ header={
655
+ <Header
656
+ variant="awsui-h1-sticky"
657
+ actions={
658
+ <SpaceBetween size="xs" direction="horizontal">
659
+ <Button
660
+ data-testid="header-btn-create"
661
+ variant="primary"
662
+ onClick={() => setVisibleModal(true)}
663
+ >
664
+ Add Item
665
+ </Button>
666
+ </SpaceBetween>
667
+ }
668
+ >
669
+ Shopping list: {shoppingList?.name}
670
+ </Header>
671
+ }
672
+ >
673
+ <CreateItem
674
+ isLoading={false}
675
+ title="Add Item"
676
+ callback={async (item) => {
677
+ const items = [
678
+ ...(shoppingItems || []),
679
+ {
680
+ id: item,
681
+ definition: { minColumnSpan: 4 },
682
+ data: { name: item },
683
+ },
684
+ ];
685
+ setShoppingItems(items);
686
+ putShoppingList.mutate({
687
+ putShoppingListRequestContent: {
688
+ name: shoppingList.name,
689
+ shoppingListId: shoppingList.shoppingListId,
690
+ shoppingItems: items.map((i) => i.data.name),
691
+ },
692
+ });
693
+ }}
694
+ visibleModal={visibleModal}
695
+ setVisibleModal={setVisibleModal}
696
+ />
697
+ <Container>
698
+ {!shoppingList ? (
699
+ <Spinner />
700
+ ) : (
701
+ <Board<ListItem>
702
+ onItemsChange={(event) => {
703
+ const items = event.detail.items as BoardProps.Item<ListItem>[];
704
+ setShoppingItems(items);
705
+ putShoppingList.mutate({
706
+ putShoppingListRequestContent: {
707
+ name: shoppingList.name,
708
+ shoppingListId: shoppingList.shoppingListId,
709
+ shoppingItems: items.map((i) => i.data.name),
710
+ },
711
+ });
712
+ }}
713
+ items={shoppingItems || []}
714
+ renderItem={(item, actions) => (
715
+ <BoardItem
716
+ header={item.data.name}
717
+ settings={
718
+ <Button
719
+ iconName="close"
720
+ variant="icon"
721
+ onClick={actions.removeItem}
722
+ />
723
+ }
724
+ i18nStrings={{
725
+ dragHandleAriaLabel: "Drag handle",
726
+ dragHandleAriaDescription:
727
+ "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.",
728
+ resizeHandleAriaLabel: "Resize handle",
729
+ resizeHandleAriaDescription:
730
+ "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.",
731
+ }}
732
+ />
733
+ )}
734
+ i18nStrings={{
735
+ liveAnnouncementDndCommitted: () => "",
736
+ liveAnnouncementDndDiscarded: () => "",
737
+ liveAnnouncementDndItemInserted: () => "",
738
+ liveAnnouncementDndItemReordered: () => "",
739
+ liveAnnouncementDndItemResized: () => "",
740
+ liveAnnouncementDndStarted: () => "",
741
+ liveAnnouncementItemRemoved: () => "",
742
+ navigationAriaLabel: "",
743
+ navigationItemAriaLabel: () => "",
744
+ }}
745
+ empty={<></>}
746
+ />
747
+ )}
748
+ </Container>
749
+ </ContentLayout>
750
+ );
751
+ };
752
+
753
+ export default ShoppingList;
754
+ ```
755
+ </TabItem>
756
+ <TabItem label="After">
757
+ ```tsx
758
+ // routes/$shoppingListId.tsx
759
+ /* eslint-disable @typescript-eslint/no-floating-promises */
760
+ import {
761
+ Board,
762
+ BoardItem,
763
+ BoardProps,
764
+ } from "@cloudscape-design/board-components";
765
+ import {
766
+ Button,
767
+ Container,
768
+ ContentLayout,
769
+ Header,
770
+ SpaceBetween,
771
+ Spinner,
772
+ } from "@cloudscape-design/components";
773
+ import { useEffect, useState } from "react";
774
+ import CreateItem from "../components/CreateItem";
775
+ import { createFileRoute } from "@tanstack/react-router";
776
+ import { useMyApi } from "../hooks/useMyApi";
777
+ import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
778
+ import { ShoppingList as _ShoppingList } from "../generated/my-api/types.gen";
779
+
780
+ type ListItem = { name: string };
781
+
782
+ /**
783
+ * Component to render a singular Shopping List "/:shoppingListId" route.
784
+ */
785
+ const ShoppingList: React.FC = () => {
786
+ const { shoppingListId } = Route.useParams();
787
+ const [visibleModal, setVisibleModal] = useState(false);
788
+ const api = useMyApi();
789
+ const getShoppingLists = useInfiniteQuery(
790
+ api.getShoppingLists.infiniteQueryOptions(
791
+ { shoppingListId },
792
+ { getNextPageParam: (p) => p.nextToken },
793
+ ),
794
+ );
795
+ const putShoppingList = useMutation(api.putShoppingList.mutationOptions());
796
+ const shoppingList: _ShoppingList | undefined =
797
+ getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0];
798
+ const [shoppingItems, setShoppingItems] =
799
+ useState<BoardProps.Item<ListItem>[]>();
800
+
801
+ useEffect(() => {
802
+ setShoppingItems(
803
+ shoppingList?.shoppingItems?.map((i) => ({
804
+ id: i,
805
+ definition: { minColumnSpan: 4 },
806
+ data: { name: i },
807
+ })),
808
+ );
809
+ }, [shoppingList?.shoppingItems]);
810
+
811
+ return (
812
+ <ContentLayout
813
+ header={
814
+ <Header
815
+ variant="awsui-h1-sticky"
816
+ actions={
817
+ <SpaceBetween size="xs" direction="horizontal">
818
+ <Button
819
+ data-testid="header-btn-create"
820
+ variant="primary"
821
+ onClick={() => setVisibleModal(true)}
822
+ >
823
+ Add Item
824
+ </Button>
825
+ </SpaceBetween>
826
+ }
827
+ >
828
+ Shopping list: {shoppingList?.name}
829
+ </Header>
830
+ }
831
+ >
832
+ <CreateItem
833
+ isLoading={false}
834
+ title="Add Item"
835
+ callback={async (item) => {
836
+ const items = [
837
+ ...(shoppingItems || []),
838
+ {
839
+ id: item,
840
+ definition: { minColumnSpan: 4 },
841
+ data: { name: item },
842
+ },
843
+ ];
844
+ setShoppingItems(items);
845
+ putShoppingList.mutate({
846
+ name: shoppingList?.name ?? 'my list',
847
+ shoppingListId: shoppingList?.shoppingListId,
848
+ shoppingItems: items.map((i) => i.data.name),
849
+ });
850
+ }}
851
+ visibleModal={visibleModal}
852
+ setVisibleModal={setVisibleModal}
853
+ />
854
+ <Container>
855
+ {!shoppingList ? (
856
+ <Spinner />
857
+ ) : (
858
+ <Board<ListItem>
859
+ onItemsChange={(event) => {
860
+ const items = event.detail.items as BoardProps.Item<ListItem>[];
861
+ setShoppingItems(items);
862
+ putShoppingList.mutate({
863
+ name: shoppingList.name,
864
+ shoppingListId: shoppingList.shoppingListId,
865
+ shoppingItems: items.map((i) => i.data.name),
866
+ });
867
+ }}
868
+ items={shoppingItems || []}
869
+ renderItem={(item, actions) => (
870
+ <BoardItem
871
+ header={item.data.name}
872
+ settings={
873
+ <Button
874
+ iconName="close"
875
+ variant="icon"
876
+ onClick={actions.removeItem}
877
+ />
878
+ }
879
+ i18nStrings={{
880
+ dragHandleAriaLabel: "Drag handle",
881
+ dragHandleAriaDescription:
882
+ "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.",
883
+ resizeHandleAriaLabel: "Resize handle",
884
+ resizeHandleAriaDescription:
885
+ "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.",
886
+ }}
887
+ />
888
+ )}
889
+ i18nStrings={{
890
+ liveAnnouncementDndCommitted: () => "",
891
+ liveAnnouncementDndDiscarded: () => "",
892
+ liveAnnouncementDndItemInserted: () => "",
893
+ liveAnnouncementDndItemReordered: () => "",
894
+ liveAnnouncementDndItemResized: () => "",
895
+ liveAnnouncementDndStarted: () => "",
896
+ liveAnnouncementItemRemoved: () => "",
897
+ navigationAriaLabel: "",
898
+ navigationItemAriaLabel: () => "",
899
+ }}
900
+ empty={<></>}
901
+ />
902
+ )}
903
+ </Container>
904
+ </ContentLayout>
905
+ );
906
+ };
907
+
908
+ export const Route = createFileRoute('/$shoppingListId')({
909
+ component: ShoppingList,
910
+ });
911
+ ```
912
+ </TabItem>
913
+ </Tabs>
914
+
915
+ </Drawer>