diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9428b2b3db..3c9928c2ad9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -188,11 +188,11 @@ To run a single test in VSCode, do any one of: - Unix/macOS/POSIX shell: ``` - TEST_FILE=src/test/foo.test.ts npm run test + TEST_FILE=../core/src/test/foo.test.ts npm run test ``` - Powershell: ``` - $Env:TEST_FILE = "src/test/foo.test.ts"; npm run test + $Env:TEST_FILE = "../core/src/test/foo.test.ts"; npm run test ``` - To run all tests in a particular subdirectory, you can edit @@ -209,11 +209,11 @@ To run tests against a specific folder in VSCode, do any one of: - Run in your terminal - Unix/macOS/POSIX shell: ``` - TEST_DIR=src/test/foo npm run test + TEST_DIR=../core/src/test/foo npm run test ``` - Powershell: ``` - $Env:TEST_DIR = "src/test/foo"; npm run test + $Env:TEST_DIR = "../core/src/test/foo"; npm run test ``` ### Coverage report diff --git a/package-lock.json b/package-lock.json index cffbb25cd00..8f0df71a2b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "plugins/*" ], "dependencies": { - "@types/node": "^18.19.55", + "@aws-toolkits/telemetry": "^1.0.242", + "@types/node": "^22.7.5", "vscode-nls": "^5.2.0", "vscode-nls-dev": "^4.0.4" }, @@ -230,6 +231,866 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-cloudformation": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.682.0.tgz", + "integrity": "sha512-RJVzgm9Q15yWnU4mFiHEO1M7k8d5ARgDhGkKSz5sE10dzG5vKockqkhgvwBgmwiAgkfnSkI/Bzx3/baBMm9BVQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/client-sts": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.6", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.682.0.tgz", + "integrity": "sha512-PYH9RFUMYLFl66HSBq4tIx6fHViMLkhJHTYJoJONpBs+Td+NwVJ895AdLtDsBIhMS0YseCbPpuyjUCJgsUrwUw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", + "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sts": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", + "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/core": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.679.0.tgz", + "integrity": "sha512-CS6PWGX8l4v/xyvX8RtXnBisdCa5+URzKd0L6GvHChype9qKUVxO/Gg6N/y43Hvg7MNWJt9FBPNWIxUB+byJwg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.679.0.tgz", + "integrity": "sha512-EdlTYbzMm3G7VUNAMxr9S1nC1qUNqhKlAxFU8E7cKsAe8Bp29CD5HAs3POc56AVo9GC4yRIS+/mtlZSmrckzUA==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.679.0.tgz", + "integrity": "sha512-ZoKLubW5DqqV1/2a3TSn+9sSKg0T8SsYMt1JeirnuLJF0mCoYFUaWMyvxxKuxPoqvUsaycxKru4GkpJ10ltNBw==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.682.0.tgz", + "integrity": "sha512-6eqWeHdK6EegAxqDdiCi215nT3QZPwukgWAYuVxNfJ/5m0/P7fAzF+D5kKVgByUvGJEbq/FEL8Fw7OBe64AA+g==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.682.0.tgz", + "integrity": "sha512-HSmDqZcBVZrTctHCT9m++vdlDfJ1ARI218qmZa+TZzzOFNpKWy6QyHMEra45GB9GnkkMmV6unoDSPMuN0AqcMg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.679.0.tgz", + "integrity": "sha512-u/p4TV8kQ0zJWDdZD4+vdQFTMhkDEJFws040Gm113VHa/Xo1SYOjbpvqeuFoz6VmM0bLvoOWjxB9MxnSQbwKpQ==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.682.0.tgz", + "integrity": "sha512-h7IH1VsWgV6YAJSWWV6y8uaRjGqLY3iBpGZlXuTH/c236NMLaNv+WqCBLeBxkFGUb2WeQ+FUPEJDCD69rgLIkg==", + "dependencies": { + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/token-providers": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.679.0.tgz", + "integrity": "sha512-a74tLccVznXCaBefWPSysUcLXYJiSkeUmQGtalNgJ1vGkE36W5l/8czFiiowdWdKWz7+x6xf0w+Kjkjlj42Ung==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.679.0.tgz", + "integrity": "sha512-y176HuQ8JRY3hGX8rQzHDSbCl9P5Ny9l16z4xmaiLo+Qfte7ee4Yr3yaAKd7GFoJ3/Mhud2XZ37fR015MfYl2w==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-logger": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.679.0.tgz", + "integrity": "sha512-0vet8InEj7nvIvGKk+ch7bEF5SyZ7Us9U7YTEgXPrBNStKeRUsgwRm0ijPWWd0a3oz2okaEwXsFl7G/vI0XiEA==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.679.0.tgz", + "integrity": "sha512-sQoAZFsQiW/LL3DfKMYwBoGjYDEnMbA9WslWN8xneCmBAwKo6IcSksvYs23PP8XMIoBGe2I2J9BSr654XWygTQ==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.682.0.tgz", + "integrity": "sha512-7TyvYR9HdGH1/Nq0eeApUTM4izB6rExiw87khVYuJwZHr6FmvIL1FsOVFro/4WlXa0lg4LiYOm/8H8dHv+fXTg==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.679.0.tgz", + "integrity": "sha512-Ybx54P8Tg6KKq5ck7uwdjiKif7n/8g1x+V0V9uTjBjRWqaIgiqzXwKWoPj6NCNkE7tJNtqI4JrNxp/3S3HvmRw==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/token-providers": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.679.0.tgz", + "integrity": "sha512-1/+Zso/x2jqgutKixYFQEGli0FELTgah6bm7aB+m2FAWH4Hz7+iMUsazg6nSWm714sG9G3h5u42Dmpvi9X6/hA==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", + "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-endpoints": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.679.0.tgz", + "integrity": "sha512-YL6s4Y/1zC45OvddvgE139fjeWSKKPgLlnfrvhVL7alNyY9n7beR4uhoDpNrt5mI6sn9qiBF17790o+xLAXjjg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.679.0.tgz", + "integrity": "sha512-CusSm2bTBG1kFypcsqU8COhnYc6zltobsqs3nRrvYqYaOqtMnuE46K4XTWpnzKgwDejgZGOE+WYyprtAxrPvmQ==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.682.0.tgz", + "integrity": "sha512-so5s+j0gPoTS0HM4HPL+G0ajk0T6cQAg8JXzRgvyiQAxqie+zGCZAV3VuVeMNWMVbzsgZl0pYZaatPFTLG/AxA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/abort-controller": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", + "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/middleware-endpoint": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", + "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-middleware": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/middleware-retry": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", + "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/middleware-serde": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", + "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/middleware-stack": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", + "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/node-config-provider": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", + "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", + "dependencies": { + "@smithy/property-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/node-http-handler": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", + "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", + "dependencies": { + "@smithy/abort-controller": "^3.1.6", + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/property-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", + "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/protocol-http": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz", + "integrity": "sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/querystring-builder": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", + "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/querystring-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", + "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/service-error-classification": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", + "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", + "dependencies": { + "@smithy/types": "^3.6.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", + "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/smithy-client": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", + "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-stream": "^3.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/url-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", + "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-middleware": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", + "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-retry": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", + "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", + "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", + "dependencies": { + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", + "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-cognito-identity": { "version": "3.637.0", "license": "Apache-2.0", @@ -6837,11 +7698,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "3.1.2", - "license": "Apache-2.0", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.7.tgz", + "integrity": "sha512-d5yGlQtmN/z5eoTtIYgkvOw27US2Ous4VycnXatyoImIF9tzlcpnKqQ/V7qhvJmb2p6xZne1NopCLakdTnkBBQ==", "dependencies": { - "@smithy/abort-controller": "^3.1.1", - "@smithy/types": "^3.3.0", + "@smithy/abort-controller": "^3.1.6", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -6849,10 +7711,11 @@ } }, "node_modules/@smithy/util-waiter/node_modules/@smithy/abort-controller": { - "version": "3.1.1", - "license": "Apache-2.0", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", + "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { @@ -6860,8 +7723,9 @@ } }, "node_modules/@smithy/util-waiter/node_modules/@smithy/types": { - "version": "3.3.0", - "license": "Apache-2.0", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", "dependencies": { "tslib": "^2.6.2" }, @@ -7190,12 +8054,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", - "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", - "license": "MIT", + "version": "22.8.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.4.tgz", + "integrity": "sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@types/node-fetch": { @@ -7332,7 +8195,6 @@ "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, "license": "MIT" }, "node_modules/@types/vscode": { @@ -17724,10 +18586,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unescape-html": { "version": "1.1.0", @@ -19155,6 +20016,7 @@ "dependencies": { "@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client", "@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming", + "@aws-sdk/client-cloudformation": "^3.667.0", "@aws-sdk/client-cognito-identity": "^3.637.0", "@aws-sdk/client-lambda": "^3.637.0", "@aws-sdk/client-sso": "^3.342.0", diff --git a/package.json b/package.json index 37990493f6c..d9fb5da27d5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "webpack-merge": "^5.10.0" }, "dependencies": { - "@types/node": "^18.19.55", + "@types/node": "^22.7.5", + "@aws-toolkits/telemetry": "^1.0.242", "vscode-nls": "^5.2.0", "vscode-nls-dev": "^4.0.4" } diff --git a/packages/core/package.json b/packages/core/package.json index 8e3f469e24d..f74f4b8482e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -461,6 +461,7 @@ "@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming", "@aws-sdk/client-cognito-identity": "^3.637.0", "@aws-sdk/client-lambda": "^3.637.0", + "@aws-sdk/client-cloudformation": "^3.667.0", "@aws-sdk/client-sso": "^3.342.0", "@aws-sdk/client-sso-oidc": "^3.574.0", "@aws-sdk/credential-provider-ini": "3.46.0", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index af49d8eb963..0921816760f 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -79,9 +79,18 @@ "AWS.configuration.description.amazonq.workspaceIndexUseGPU": "Enable GPU to help index your local workspace files. Only applies to Linux and Windows.", "AWS.configuration.description.amazonq.workspaceIndexMaxSize": "The maximum size of local workspace files to be indexed in MB", "AWS.command.apig.copyUrl": "Copy URL", - "AWS.command.apig.invokeRemoteRestApi": "Invoke on AWS", + "AWS.command.apig.invokeRemoteRestApi": "Invoke in the cloud", "AWS.command.apig.invokeRemoteRestApi.cn": "Invoke on Amazon", + "AWS.appBuilder.explorerTitle": "Application Builder", + "AWS.appBuilder.explorerNode.noApps": "[This resource is not yet supported.]", + "AWS.appBuilder.explorerNode.unavailableDeployedResource": "[Failed to retrive deployed resource.]", + "AWS.command.appBuilder.openHandler": "Open Function Handler", "AWS.command.applicationComposer.open": "Open with Infrastructure Composer", + "AWS.command.appBuilder.openTemplate": "Open Template File", + "AWS.command.appBuilder.deploy": "Deploy SAM Application", + "AWS.command.appBuilder.build": "Build SAM Template", + "AWS.command.appBuilder.searchLogs": "Search Logs", + "AWS.command.refreshappBuilderExplorer": "Refresh Application Builder Explorer", "AWS.command.applicationComposer.openDialog": "Open Template with Infrastructure Composer...", "AWS.command.auth.addConnection": "Add New Connection", "AWS.command.auth.showConnectionsPage": "Add New Connection", @@ -118,7 +127,7 @@ "AWS.command.aboutToolkit": "About", "AWS.command.downloadLambda": "Download...", "AWS.command.uploadLambda": "Upload Lambda...", - "AWS.command.invokeLambda": "Invoke on AWS", + "AWS.command.invokeLambda": "Invoke in the cloud", "AWS.command.invokeLambda.cn": "Invoke on Amazon", "AWS.command.refreshAwsExplorer": "Refresh Explorer", "AWS.command.refreshCdkExplorer": "Refresh CDK Explorer", @@ -148,6 +157,8 @@ "AWS.command.renderStateMachineGraph": "Render graph", "AWS.command.copyArn": "Copy ARN", "AWS.command.copyName": "Copy Name", + "AWS.command.openAwsConsole": "Go to AWS management console", + "AWS.command.openAwsConsole.cn": "Go to Amazon management console", "AWS.command.listCommands": "Show AWS Commands...", "AWS.command.listCommands.cn": "Show Amazon Commands...", "AWS.command.downloadStateMachineDefinition": "Download Definition...", @@ -198,8 +209,8 @@ "AWS.command.ssmDocument.openLocalDocumentJson": "Download as JSON", "AWS.command.ssmDocument.openLocalDocumentYaml": "Download as YAML", "AWS.command.ssmDocument.publishDocument": "Publish a Systems Manager Document", - "AWS.command.launchConfigForm.title": "Edit SAM Debug Configuration", - "AWS.command.addSamDebugConfig": "Add SAM Debug Configuration", + "AWS.command.launchConfigForm.title": "Local Invoke and Debug Configuration", + "AWS.command.addSamDebugConfig": "Add Local Invoke and Debug Configuration", "AWS.command.toggleSamCodeLenses": "Toggle SAM hints in source files", "AWS.command.apprunner.createService": "Create Service", "AWS.command.apprunner.createServiceFromEcr": "Create App Runner Service", diff --git a/packages/core/resources/icons/aws/applicationcomposer/icon-dark.svg b/packages/core/resources/icons/aws/applicationcomposer/icon-dark.svg index 632d645338e..c631f327ae0 100644 --- a/packages/core/resources/icons/aws/applicationcomposer/icon-dark.svg +++ b/packages/core/resources/icons/aws/applicationcomposer/icon-dark.svg @@ -2,6 +2,6 @@ Icon-Service/16/AWS-Application-Composer_16_White - + diff --git a/packages/core/resources/icons/aws/applicationcomposer/icon.svg b/packages/core/resources/icons/aws/applicationcomposer/icon.svg index f2b405cfe57..65580136ce7 100644 --- a/packages/core/resources/icons/aws/applicationcomposer/icon.svg +++ b/packages/core/resources/icons/aws/applicationcomposer/icon.svg @@ -2,6 +2,6 @@ Icon-Service/16/AWS-Application-Composer_16 - + diff --git a/packages/core/resources/markdown/samReadme.md b/packages/core/resources/markdown/samReadme.md index e98229d9b5c..14022174844 100644 --- a/packages/core/resources/markdown/samReadme.md +++ b/packages/core/resources/markdown/samReadme.md @@ -13,9 +13,9 @@ ${LISTOFCONFIGURATIONS} You can debug the Lambda handlers locally by adding a breakpoint to the source file, then running the launch configuration. This works by using Docker on your local machine. -Invocation parameters, including payloads and request parameters, can be edited either by the `Edit SAM Debug Configuration` command (through the ${COMMANDPALETTE} or ${CODELENS}) or by editing the `launch.json` file. +Invocation parameters, including payloads and request parameters, can be edited either by the `Local Invoke and Debug Configuration` command (through the ${COMMANDPALETTE} or ${CODELENS}) or by editing the `launch.json` file. -${COMPANYNAME} Lambda functions not defined in the [`template.yaml`](./template.yaml) file can be invoked and debugged by creating a launch configuration through the ${CODELENS} over the function declaration, or with the `Add SAM Debug Configuration` command. +${COMPANYNAME} Lambda functions not defined in the [`template.yaml`](./template.yaml) file can be invoked and debugged by creating a launch configuration through the ${CODELENS} over the function declaration, or with the `Add Local Invoke and Debug Configuration` command. ## Deploying Serverless Applications diff --git a/packages/core/resources/walkthrough/appBuilder/AppPicker.md b/packages/core/resources/walkthrough/appBuilder/AppPicker.md new file mode 100644 index 00000000000..dbb78f0108d --- /dev/null +++ b/packages/core/resources/walkthrough/appBuilder/AppPicker.md @@ -0,0 +1,25 @@ + +
+ + + Rest API + + + + File processing + +
+
+ + + New template with visual builder + + + + Current workspace template + +
+
+ + See more application example... + diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/API.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/API.png new file mode 100644 index 00000000000..3cda59fbf48 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/API.png differ diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/AppComposer.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/AppComposer.png new file mode 100644 index 00000000000..51ceb392f05 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/AppComposer.png differ diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/CustomTemplate.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/CustomTemplate.png new file mode 100644 index 00000000000..6ebe6fe27a6 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/CustomTemplate.png differ diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/S3.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/S3.png new file mode 100644 index 00000000000..7ea7eebd23b Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/S3.png differ diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoop.md b/packages/core/resources/walkthrough/appBuilder/InnerLoop.md new file mode 100644 index 00000000000..97f54e9dbca --- /dev/null +++ b/packages/core/resources/walkthrough/appBuilder/InnerLoop.md @@ -0,0 +1,12 @@ +

Build your code

+Compile your code and install dependencies with SAM CLI so you can invoke it locally. +clicking build icon in AppBuilder sidebar on project node +

Select function to invoke

+Find the function you want to invoke in Application Builder and use the icon to open the invoke and debug view. +clicking invoke function icon in AppBuilder sidebar on Lambda function node +

Invoke your function

+Configure a payload to use for invoking your function. +clicking invoke function button in local invoke webview +

View your execution results

+The VS Code panel will display the results of your invocation. +View your execution results in the output panel diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-1.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-1.jpg new file mode 100644 index 00000000000..b1236191118 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-1.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-2.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-2.jpg new file mode 100644 index 00000000000..d7c0295e5ce Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-2.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-3.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-3.jpg new file mode 100644 index 00000000000..471cf590830 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-3.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-4.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-4.jpg new file mode 100644 index 00000000000..c0df6db61b0 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-4.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoop.md b/packages/core/resources/walkthrough/appBuilder/RemoteLoop.md new file mode 100644 index 00000000000..f811771127b --- /dev/null +++ b/packages/core/resources/walkthrough/appBuilder/RemoteLoop.md @@ -0,0 +1,12 @@ +

Deploy your application

+Use SAM CLI to deploy your application template to the cloud. +Click Deploy SAM Application button in Appbuilder Sidebar on project node +

Select deployed function to invoke

+Find the function you want to invoke in Application Builder and use the icon to open the remote invocation view. +click Invoke in the cloud button in AppBuilder Sidebar on cloud function resource node +

Invoke your function with a payload

+Configure a payload to use for invoking your function. +add a payload and click invoke button in remote invoke panel +

View your execution result in the output panel

+The VS Code panel will display the results of your invocation. +View your execution result in the output panel diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-1.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-1.jpg new file mode 100644 index 00000000000..c036fc93eaf Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-1.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-2.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-2.jpg new file mode 100644 index 00000000000..e542edc38f4 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-2.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-3.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-3.jpg new file mode 100644 index 00000000000..891cfdc4aa0 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-3.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-4.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-4.jpg new file mode 100644 index 00000000000..738a7d0b91f Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-4.jpg differ diff --git a/packages/core/resources/walkthrough/appBuilder/install.png b/packages/core/resources/walkthrough/appBuilder/install.png new file mode 100644 index 00000000000..cdbae8b83a6 Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/install.png differ diff --git a/packages/core/resources/walkthrough/setup-connect.md b/packages/core/resources/walkthrough/setup-connect.md index 44a1584838a..e78f612f4f7 100644 --- a/packages/core/resources/walkthrough/setup-connect.md +++ b/packages/core/resources/walkthrough/setup-connect.md @@ -12,7 +12,7 @@ Choose the most appropriate method based on your requirements. ## Connect to AWS through the Toolkit for VS Code -1. [Click here](command:aws.login) to open the configuration wizard to connect to AWS. +1. [Click here](command:aws.toolkit.login) to open the configuration wizard to connect to AWS. > This command can also be accessed through the [Command Palette](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/toolkit-navigation.html#command-locations) by choosing **AWS: >Connect to AWS**\. > diff --git a/packages/core/resources/walkthrough/setup-region.md b/packages/core/resources/walkthrough/setup-region.md index 292eb1ad220..50a9101e6bb 100644 --- a/packages/core/resources/walkthrough/setup-region.md +++ b/packages/core/resources/walkthrough/setup-region.md @@ -4,7 +4,7 @@ When you set up your credentials, the AWS Toolkit for Visual Studio Code automat ## Add a Region to the AWS Explorer -1. [Click here](command:aws.showRegion) to select a Region to add or remove. +1. [Click here](command:aws.toolkit.showRegion) to select a Region to add or remove. > This command can also be accessed through the [Command Palette](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/toolkit-navigation.html#command-locations) by choosing **AWS: Show or Hide Regions**\. > diff --git a/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts b/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts index ed50e4ee2da..351673754c2 100644 --- a/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts +++ b/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts @@ -8,11 +8,13 @@ import { ApplicationComposerManager } from '../webviewManager' import vscode from 'vscode' import { telemetry } from '../../shared/telemetry/telemetry' import { ToolkitError } from '../../shared/errors' +import { isTreeNode, TreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { SamAppLocation } from '../../awsService/appBuilder/explorer/samProject' import { getAmazonqApi } from '../../amazonq/extApi' export const openTemplateInComposerCommand = Commands.declare( 'aws.openInApplicationComposer', - (manager: ApplicationComposerManager) => async (arg?: vscode.TextEditor | vscode.Uri) => { + (manager: ApplicationComposerManager) => async (arg?: vscode.TextEditor | vscode.Uri | TreeNode) => { let result: vscode.WebviewPanel | undefined await telemetry.appcomposer_openTemplate.run(async (span) => { const amazonqApi = await getAmazonqApi() @@ -26,8 +28,14 @@ export const openTemplateInComposerCommand = Commands.declare( span.record({ hasChatAuth, }) - arg ??= vscode.window.activeTextEditor - const input = arg instanceof vscode.Uri ? arg : arg?.document + let input = undefined + if (arg instanceof vscode.Uri) { + input = arg + } else if (isTreeNode(arg)) { + input = ((arg as TreeNode).resource as SamAppLocation).samTemplateUri + } else { + input = vscode.window.activeTextEditor?.document + } if (!input) { throw new ToolkitError('No active text editor or document found') diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index 9b7eaca7592..0cd33ef6360 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -57,6 +57,8 @@ import { SharedCredentialsProviderFactory } from './providers/sharedCredentialsP import { Ec2CredentialsProvider } from './providers/ec2CredentialsProvider' import { EcsCredentialsProvider } from './providers/ecsCredentialsProvider' import { EnvVarsCredentialsProvider } from './providers/envVarsCredentialsProvider' +import { showMessageWithUrl } from '../shared/utilities/messages' +import { credentialHelpUrl } from '../shared/constants' // iam-only excludes Builder ID and IAM Identity Center from the list of valid connections // TODO: Understand if "iam" should include these from the list at all @@ -106,6 +108,46 @@ export async function promptAndUseConnection(...[auth, type]: Parameters { return telemetry.function_call.run( async () => { diff --git a/packages/core/src/awsService/apigateway/activation.ts b/packages/core/src/awsService/apigateway/activation.ts index 2bf2f44ea4c..78add0d3e67 100644 --- a/packages/core/src/awsService/apigateway/activation.ts +++ b/packages/core/src/awsService/apigateway/activation.ts @@ -9,6 +9,8 @@ import { invokeRemoteRestApi } from './vue/invokeRemoteRestApi' import { copyUrlCommand } from './commands/copyUrl' import { ExtContext } from '../../shared/extensions' import { Commands } from '../../shared/vscode/commands2' +import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { getSourceNode } from '../../shared/utilities/treeNodeUtils' /** * Activate API Gateway functionality for the extension. @@ -20,14 +22,16 @@ export async function activate(activateArguments: { const extensionContext = activateArguments.extContext.extensionContext const regionProvider = activateArguments.extContext.regionProvider extensionContext.subscriptions.push( - Commands.register('aws.apig.copyUrl', async (node: RestApiNode) => await copyUrlCommand(node, regionProvider)), - Commands.register( - 'aws.apig.invokeRemoteRestApi', - async (node: RestApiNode) => - await invokeRemoteRestApi(activateArguments.extContext, { - apiNode: node, - outputChannel: activateArguments.outputChannel, - }) - ) + Commands.register('aws.apig.copyUrl', async (node: RestApiNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await copyUrlCommand(sourceNode, regionProvider) + }), + Commands.register('aws.apig.invokeRemoteRestApi', async (node: RestApiNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await invokeRemoteRestApi(activateArguments.extContext, { + apiNode: sourceNode, + outputChannel: activateArguments.outputChannel, + }) + }) ) } diff --git a/packages/core/src/awsService/appBuilder/activation.ts b/packages/core/src/awsService/appBuilder/activation.ts new file mode 100644 index 00000000000..9f30282573c --- /dev/null +++ b/packages/core/src/awsService/appBuilder/activation.ts @@ -0,0 +1,205 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import globals from '../../shared/extensionGlobals' +import { ExtContext } from '../../shared/extensions' +import { Commands, VsCodeCommandArg } from '../../shared/vscode/commands2' +import { ToolView } from '../../awsexplorer/toolView' +import { telemetry } from '../../shared/telemetry/telemetry' +import { activateViewsShared, registerToolView } from '../../awsexplorer/activationShared' +import { setContext } from '../../shared/vscode/setContext' +import { fs } from '../../shared/fs/fs' +import { AppBuilderRootNode } from './explorer/nodes/rootNode' +import { initWalkthroughProjectCommand, walkthroughContextString, getOrInstallCliWrapper } from './walkthrough' +import { getLogger } from '../../shared/logger' +import path from 'path' +import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { runBuild } from '../../shared/sam/build' +import { runOpenHandler, runOpenTemplate } from './utils' +import { ResourceNode } from './explorer/nodes/resourceNode' +import { getSyncWizard, runSync } from '../../shared/sam/sync' +import { getDeployWizard, runDeploy } from '../../shared/sam/deploy' +import { DeployTypeWizard } from './wizards/deployTypeWizard' + +export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpenOnStart' + +/** + * Activates the AWS Explorer UI and related functionality. + * + * IMPORTANT: Views that should work in all vscode environments (node or web) + * should be setup in {@link activateViewsShared}. + */ +export async function activate(context: ExtContext): Promise { + // recover context variables from global state when activate + const walkthroughSelected = globals.globalState.get(walkthroughContextString) + if (walkthroughSelected !== undefined) { + await setContext(walkthroughContextString, walkthroughSelected) + } + + await registerAppBuilderCommands(context) + + const appBuilderNode: ToolView[] = [ + { + nodes: [AppBuilderRootNode.instance], + view: 'aws.appBuilder', + refreshCommands: [AppBuilderRootNode.instance.refreshAppBuilderExplorer], + }, + { + nodes: [AppBuilderRootNode.instance], + view: 'aws.appBuilderForFileExplorer', + refreshCommands: [AppBuilderRootNode.instance.refreshAppBuilderForFileExplorer], + }, + ] + + const watcher = vscode.workspace.createFileSystemWatcher('**/{template.yaml,template.yml,samconfig.toml}') + watcher.onDidChange(async (uri) => runRefreshAppBuilder(uri, 'changed')) + watcher.onDidCreate(async (uri) => runRefreshAppBuilder(uri, 'created')) + watcher.onDidDelete(async (uri) => runRefreshAppBuilder(uri, 'deleted')) + + for (const viewNode of appBuilderNode) { + registerToolView(viewNode, context.extensionContext) + } + + await openApplicationComposerAfterReload() +} + +async function runRefreshAppBuilder(uri: vscode.Uri, event: string) { + getLogger().debug(`${uri.fsPath} ${event}, refreshing appBuilder`) + await vscode.commands.executeCommand('aws.appBuilderForFileExplorer.refresh') + await vscode.commands.executeCommand('aws.appBuilder.refresh') +} + +/** + * To support open template in AppComposer after extension reload. + * This typically happens when user create project from walkthrough + * and added a new folder to an empty workspace. + * + * Checkes templateToOpenAppComposer in global and opens template + * Directly return if templateToOpenAppComposer is undefined + */ +export async function openApplicationComposerAfterReload(): Promise { + const templatesToOpen = globals.globalState.get<[string]>(templateToOpenAppComposer) + // undefined + if (!templatesToOpen) { + return + } + + for (const template of templatesToOpen) { + const templateUri = vscode.Uri.file(template) + const templateFolder = vscode.Uri.file(path.dirname(template)) + const basename = path.basename(template) + // ignore templates that doesn't belong to current workspace, ignore if not template + if ( + !vscode.workspace.getWorkspaceFolder(templateFolder) || + (basename !== 'template.yaml' && basename !== 'template.yml') + ) { + continue + } + + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') + await vscode.commands.executeCommand('aws.openInApplicationComposer', templateUri) + + if (await fs.exists(vscode.Uri.joinPath(templateFolder, 'README.md'))) { + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') + await vscode.commands.executeCommand( + 'markdown.showPreview', + vscode.Uri.joinPath(templateFolder, 'README.md') + ) + } + } + // set to undefined + await globals.globalState.update(templateToOpenAppComposer, undefined) +} + +async function setWalkthrough(walkthroughSelected: string = 'S3'): Promise { + await setContext(walkthroughContextString, walkthroughSelected) + await globals.globalState.update(walkthroughContextString, walkthroughSelected) +} + +/** + * + * @param context VScode Context + */ +async function registerAppBuilderCommands(context: ExtContext): Promise { + const source = 'AppBuilderWalkthrough' + context.extensionContext.subscriptions.push( + Commands.register('aws.toolkit.installSAMCLI', async () => { + await getOrInstallCliWrapper('sam-cli', source) + }), + Commands.register('aws.toolkit.installAWSCLI', async () => { + await getOrInstallCliWrapper('aws-cli', source) + }), + Commands.register('aws.toolkit.installDocker', async () => { + await getOrInstallCliWrapper('docker', source) + }), + Commands.register('aws.toolkit.lambda.setWalkthroughToAPI', async () => { + await setWalkthrough('API') + }), + Commands.register('aws.toolkit.lambda.setWalkthroughToS3', async () => { + await setWalkthrough('S3') + }), + Commands.register('aws.toolkit.lambda.setWalkthroughToVisual', async () => { + await setWalkthrough('Visual') + }), + Commands.register('aws.toolkit.lambda.setWalkthroughToCustomTemplate', async () => { + await setWalkthrough('CustomTemplate') + }), + Commands.register('aws.toolkit.lambda.initializeWalkthroughProject', async (): Promise => { + await telemetry.appBuilder_selectWalkthroughTemplate.run(async () => await initWalkthroughProjectCommand()) + await globals.globalState.update('aws.toolkit.lambda.walkthroughCompleted', true) + }), + Commands.register('aws.toolkit.lambda.walkthrough.credential', async (): Promise => { + await vscode.commands.executeCommand('aws.toolkit.auth.manageConnections', source) + }), + Commands.register( + { id: `aws.toolkit.lambda.openWalkthrough`, compositeKey: { 1: 'source' } }, + async (_: VsCodeCommandArg, source?: string) => { + telemetry.appBuilder_startWalkthrough.emit({ source: source }) + await vscode.commands.executeCommand( + 'workbench.action.openWalkthrough', + 'amazonwebservices.aws-toolkit-vscode#aws.toolkit.lambda.walkthrough' + ) + } + ), + Commands.register( + { + id: 'aws.appBuilder.build', + autoconnect: false, + }, + async (arg?: TreeNode | undefined) => await telemetry.sam_build.run(async () => await runBuild(arg)) + ), + Commands.register({ id: 'aws.appBuilder.openTemplate', autoconnect: false }, async (arg: TreeNode) => + telemetry.appBuilder_openTemplate.run(async (span) => { + if (arg) { + span.record({ source: 'AppBuilderOpenTemplate' }) + } else { + span.record({ source: 'commandPalette' }) + } + await runOpenTemplate(arg) + }) + ), + Commands.register({ id: 'aws.appBuilder.openHandler', autoconnect: false }, async (arg: ResourceNode) => + telemetry.lambda_goToHandler.run(async (span) => { + span.record({ source: 'AppBuilderOpenHandler' }) + await runOpenHandler(arg) + }) + ), + Commands.register({ id: 'aws.appBuilder.deploy', autoconnect: true }, async (arg) => { + const wizard = new DeployTypeWizard( + await getSyncWizard('infra', arg, undefined, false), + await getDeployWizard(arg, false) + ) + const choices = await wizard.run() + if (choices) { + if (choices.choice === 'deploy' && choices.deployParam) { + await runDeploy(arg, choices.deployParam) + } else if (choices.choice === 'sync' && choices.syncParam) { + await runSync('infra', arg, undefined, choices.syncParam) + } + } + }) + ) +} diff --git a/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts new file mode 100644 index 00000000000..cb179d94f0d --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts @@ -0,0 +1,61 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { SamAppLocation } from './samProject' +import { getLogger } from '../../../shared/logger/logger' +import { getProjectRootUri } from '../../../shared/sam/utils' + +export async function detectSamProjects(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + + if (!workspaceFolders) { + return [] + } + + const results = new Map() + const projects = (await Promise.all(workspaceFolders.map(detectSamProjectsFromWorkspaceFolder))).reduce( + (a, b) => a.concat(b), + [] + ) + + projects.forEach((p) => results.set(p.samTemplateUri.toString(), p)) + + return Array.from(results.values()) +} + +async function detectSamProjectsFromWorkspaceFolder( + workspaceFolder: vscode.WorkspaceFolder +): Promise { + const result: SamAppLocation[] = [] + const samTemplateFiles = await getFiles(workspaceFolder, '**/template.{yml,yaml}', '**/.aws-sam/**') + for (const samTemplateFile of samTemplateFiles) { + const project = { + samTemplateUri: samTemplateFile, + workspaceFolder: workspaceFolder, + projectRoot: getProjectRootUri(samTemplateFile), + } + result.push(project) + } + return result +} + +export async function getFiles( + workspaceFolder: vscode.WorkspaceFolder, + pattern: string, + buildArtifactFolderPattern?: string +): Promise { + try { + const globPattern = new vscode.RelativePattern(workspaceFolder, pattern) + const excludePattern = buildArtifactFolderPattern + ? new vscode.RelativePattern(workspaceFolder, buildArtifactFolderPattern) + : undefined + + return await vscode.workspace.findFiles(globPattern, excludePattern) + } catch (error) { + getLogger().error(`Failed to get files with pattern ${pattern}:`, error) + return [] + } +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts new file mode 100644 index 00000000000..497e5aa22ad --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts @@ -0,0 +1,104 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as nls from 'vscode-nls' +const localize = nls.loadMessageBundle() + +import * as vscode from 'vscode' +import { getLogger } from '../../../../shared/logger' +import { ResourceTreeEntity, SamAppLocation, getApp, getStackName } from '../samProject' +import { ResourceNode, generateResourceNodes } from './resourceNode' +import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +import { createPlaceholderItem } from '../../../../shared/treeview/utils' +import { getIcon } from '../../../../shared/icons' +import { getSamCliContext } from '../../../../shared/sam/cli/samCliContext' +import { SamCliListResourcesParameters } from '../../../../shared/sam/cli/samCliListResources' +import { getDeployedResources, StackResource } from '../../../../lambda/commands/listSamResources' +import * as path from 'path' +import fs from '../../../../shared/fs/fs' +import { generateStackNode } from './deployedStack' + +export class AppNode implements TreeNode { + public readonly id = this.location.samTemplateUri.toString() + public readonly resource = this.location + public readonly label = path.join( + this.location.workspaceFolder.name, + path.relative(this.location.workspaceFolder.uri.fsPath, path.dirname(this.location.samTemplateUri.fsPath)) + ) + private stackName: string = '' + public constructor(private readonly location: SamAppLocation) {} + + public async getChildren(): Promise<(ResourceNode | TreeNode)[]> { + const resources = [] + try { + const successfulApp = await getApp(this.location) + const templateResources: ResourceTreeEntity[] = successfulApp.resourceTree + const { stackName, region } = await getStackName(this.location.projectRoot) + this.stackName = stackName + + const listStackResourcesArguments: SamCliListResourcesParameters = { + stackName: this.stackName, + templateFile: this.location.samTemplateUri.fsPath, + region: region, + projectRoot: this.location.projectRoot, + } + + const deployedResources: StackResource[] | undefined = this.stackName + ? await getDeployedResources({ + listResourcesParams: listStackResourcesArguments, + invoker: getSamCliContext().invoker, + }) + : undefined + // Skip generating stack node if stack does not exist in region or other errors + if (deployedResources && deployedResources.length > 0) { + resources.push(...(await generateStackNode(this.stackName, region))) + } + resources.push( + ...generateResourceNodes(this.location, templateResources, this.stackName, region, deployedResources) + ) + + // indicate that App exists, but it is empty + if (resources.length === 0) { + if (await fs.exists(this.location.samTemplateUri)) { + return [ + createPlaceholderItem( + localize( + 'AWS.appBuilder.explorerNode.app.noResource', + '[No resource found in IaC template]' + ) + ), + ] + } + return [ + createPlaceholderItem( + localize('AWS.appBuilder.explorerNode.app.noTemplate', '[No IaC templates found in Workspaces]') + ), + ] + } + return resources + } catch (error) { + getLogger().error(`Could not load the construct tree located at '${this.id}': %O`, error as Error) + return [ + createPlaceholderItem( + localize( + 'AWS.appBuilder.explorerNode.app.noResourceTree', + '[Unable to load Resource tree for this App. Update IaC template]' + ) + ), + ] + } + } + + public getTreeItem() { + const item = new vscode.TreeItem(this.label, vscode.TreeItemCollapsibleState.Collapsed) + + item.contextValue = 'awsAppBuilderAppNode' + item.iconPath = getIcon('vscode-folder') + item.resourceUri = this.location.samTemplateUri + item.tooltip = this.location.samTemplateUri.path + + return item + } +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts new file mode 100644 index 00000000000..70ac56bb1f6 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts @@ -0,0 +1,183 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getIcon } from '../../../../shared/icons' +import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +import { createPlaceholderItem } from '../../../../shared/treeview/utils' +import * as nls from 'vscode-nls' + +import { getLogger } from '../../../../shared/logger/logger' +import { FunctionConfiguration, LambdaClient, GetFunctionCommand } from '@aws-sdk/client-lambda' +import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' +import globals from '../../../../shared/extensionGlobals' +import { defaultPartition } from '../../../../shared/regions/regionProvider' +import { Lambda, APIGateway } from 'aws-sdk' +import { LambdaNode } from '../../../../lambda/explorer/lambdaNodes' +import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' +import { DefaultS3Client, DefaultBucket } from '../../../../shared/clients/s3Client' +import { S3Node } from '../../../../awsService/s3/explorer/s3Nodes' +import { S3BucketNode } from '../../../../awsService/s3/explorer/s3BucketNode' +import { ApiGatewayNode } from '../../../../awsService/apigateway/explorer/apiGatewayNodes' +import { RestApiNode } from '../../../../awsService/apigateway/explorer/apiNodes' +import { + SERVERLESS_FUNCTION_TYPE, + SERVERLESS_API_TYPE, + s3BucketType, +} from '../../../../shared/cloudformation/cloudformation' +import { ToolkitError } from '../../../../shared' +import { getIAMConnection } from '../../../../auth/utils' + +const localize = nls.loadMessageBundle() +export interface DeployedResource { + stackName: string + regionCode: string + explorerNode: any + arn: string + contextValue: string +} + +export const DeployedResourceContextValues: Record = { + [SERVERLESS_FUNCTION_TYPE]: 'awsRegionFunctionNodeDownloadable', + [SERVERLESS_API_TYPE]: 'awsApiGatewayNode', + [s3BucketType]: 'awsS3BucketNode', +} + +export class DeployedResourceNode implements TreeNode { + public readonly id: string + public readonly contextValue: string + + public constructor(public readonly resource: DeployedResource) { + if (this.resource.arn) { + this.id = this.resource.arn + this.contextValue = this.resource.contextValue + } else { + getLogger().warn('Cannot create DeployedResourceNode, the ARN does not exist.') + this.id = '' + this.contextValue = '' + } + } + + public async getChildren(): Promise { + return [] + } + + public getTreeItem() { + const item = new vscode.TreeItem(this.id) + + item.contextValue = this.contextValue + item.iconPath = getIcon('vscode-cloud') + item.collapsibleState = vscode.TreeItemCollapsibleState.None + item.tooltip = this.resource.arn + return item + } +} + +export async function generateDeployedNode( + deployedResource: any, + regionCode: string, + stackName: string, + resourceTreeEntity: any +): Promise { + let newDeployedResource: any + const partitionId = globals.regionProvider.getPartitionId(regionCode) ?? defaultPartition + try { + switch (resourceTreeEntity.Type) { + case SERVERLESS_FUNCTION_TYPE: { + const defaultClient = new DefaultLambdaClient(regionCode) + const lambdaNode = new LambdaNode(regionCode, defaultClient) + let configuration: Lambda.FunctionConfiguration + let v3configuration + let logGroupName + try { + configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId)) + .Configuration as Lambda.FunctionConfiguration + newDeployedResource = new LambdaFunctionNode(lambdaNode, regionCode, configuration) + } catch (error: any) { + getLogger().error('Error getting Lambda configuration') + throw ToolkitError.chain(error, 'Error getting Lambda configuration', { + code: 'lambdaClientError', + }) + } + const connection = await getIAMConnection({ prompt: false }) + if (!connection || connection.type !== 'iam') { + return [ + createPlaceholderItem( + localize( + 'AWS.appBuilder.explorerNode.unavailableDeployedResource', + '[Failed to retrive deployed resource.]' + ) + ), + ] + } + const cred = await connection.getCredentials() + const v3Client = new LambdaClient({ region: regionCode, credentials: cred }) + + const v3command = new GetFunctionCommand({ FunctionName: deployedResource.PhysicalResourceId }) + try { + v3configuration = (await v3Client.send(v3command)).Configuration as FunctionConfiguration + logGroupName = v3configuration.LoggingConfig?.LogGroup + } catch { + getLogger().error('Error getting Lambda V3 configuration') + } + newDeployedResource.configuration = { + ...newDeployedResource.configuration, + logGroupName: logGroupName, + } as any + break + } + case s3BucketType: { + const s3Client = new DefaultS3Client(regionCode) + const s3Node = new S3Node(s3Client) + const s3Bucket = new DefaultBucket({ + partitionId: partitionId, + region: regionCode, + name: deployedResource.PhysicalResourceId, + }) + newDeployedResource = new S3BucketNode(s3Bucket, s3Node, s3Client) + break + } + case SERVERLESS_API_TYPE: { + const apiParentNode = new ApiGatewayNode(partitionId, regionCode) + const apiNodes = await apiParentNode.getChildren() + const apiNode = apiNodes.find((node) => node.id === deployedResource.PhysicalResourceId) + newDeployedResource = new RestApiNode( + apiParentNode, + partitionId, + regionCode, + apiNode as APIGateway.RestApi + ) + break + } + default: + newDeployedResource = new DeployedResourceNode(deployedResource) + getLogger().info('Details are missing or are incomplete for:', deployedResource) + return [ + createPlaceholderItem( + localize('AWS.appBuilder.explorerNode.noApps', '[This resource is not yet supported.]') + ), + ] + } + } catch (error: any) { + void vscode.window.showErrorMessage(error.messages) + return [ + createPlaceholderItem( + localize( + 'AWS.appBuilder.explorerNode.unavailableDeployedResource', + '[Failed to retrive deployed resource.]' + ) + ), + ] + } + newDeployedResource.contextValue = DeployedResourceContextValues[resourceTreeEntity.Type] + const finalDeployedResource = { + stackName, + regionCode, + explorerNode: newDeployedResource, + arn: newDeployedResource.arn, + contextValue: newDeployedResource.contextValue, + } + return [new DeployedResourceNode(finalDeployedResource)] +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts new file mode 100644 index 00000000000..76c8f5ea76b --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts @@ -0,0 +1,66 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +import { getIcon } from '../../../../shared/icons' +import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation' +import { ToolkitError } from '../../../../shared' +import { getIAMConnection } from '../../../../auth/utils' + +export class StackNameNode implements TreeNode { + public readonly id = this.stackName + public readonly resource = this + public arn: string | undefined + public readonly link = `command:aws.explorer.cloudformation.showStack?${encodeURIComponent(JSON.stringify({ stackName: this.stackName, region: this.regionCode }))}` + + public constructor( + public stackName: string, + public regionCode: string + ) { + this.stackName = stackName + this.regionCode = regionCode + } + + public async getChildren(): Promise { + // This stack name node is a leaf node that does not have any children. + return [] + } + public get value(): string { + return `Stack: ${this.stackName} (${this.regionCode})` + } + + public getTreeItem() { + const item = new vscode.TreeItem(this.value) + + item.contextValue = 'awsAppBuilderStackNode' + item.iconPath = getIcon('vscode-cloud') + return item + } +} + +export async function generateStackNode(stackName?: string, regionCode?: string): Promise { + const connection = await getIAMConnection({ prompt: false }) + if (!connection || connection.type !== 'iam') { + return [] + } + const cred = await connection.getCredentials() + const client = new CloudFormationClient({ region: regionCode, credentials: cred }) + try { + const command = new DescribeStacksCommand({ StackName: stackName }) + const response = await client.send(command) + if (response.Stacks && response.Stacks[0]) { + const stackArn = response.Stacks[0].StackId + if (stackName === undefined || regionCode === undefined) { + return [] + } + const stackNode = new StackNameNode(stackName || '', regionCode || '') + stackNode.arn = stackArn + return [stackNode] + } + } catch (error) { + throw new ToolkitError(`Failed to generate stack node ${stackName} in region ${regionCode}: ${error}`) + } + return [] +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts new file mode 100644 index 00000000000..481ecdf7009 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts @@ -0,0 +1,46 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getIcon } from '../../../../shared/icons' +import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' + +export class PropertyNode implements TreeNode { + public readonly id = this.key + public readonly resource = this.value + + public constructor( + private readonly key: string, + private readonly value: unknown + ) {} + + public async getChildren(): Promise { + if (this.value instanceof Array || this.value instanceof Object) { + return generatePropertyNodes(this.value) + } else { + return [] + } + } + + public getTreeItem() { + const item = new vscode.TreeItem(`${this.key}: ${this.value}`) + + item.contextValue = 'awsAppBuilderPropertyNode' + item.iconPath = getIcon('vscode-gear') + + if (this.value instanceof Array || this.value instanceof Object) { + item.label = this.key + item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed + } + + return item + } +} + +export function generatePropertyNodes(properties: { [key: string]: any }): TreeNode[] { + return Object.entries(properties) + .filter(([key, _]) => key !== 'Id' && key !== 'Type' && key !== 'Events') + .map(([key, value]) => new PropertyNode(key, value)) +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts new file mode 100644 index 00000000000..bda7b69ac4f --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts @@ -0,0 +1,147 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { IconPath, getIcon } from '../../../../shared/icons' +import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +import { ResourceTreeEntity, SamAppLocation } from '../samProject' +import { + SERVERLESS_FUNCTION_TYPE, + s3BucketType, + appRunnerType, + ecrRepositoryType, +} from '../../../../shared/cloudformation/cloudformation' +import { generatePropertyNodes } from './propertyNode' +import { generateDeployedNode } from './deployedNode' +import { StackResource } from '../../../../lambda/commands/listSamResources' +import { DeployedResourceNode } from './deployedNode' + +enum ResourceTypeId { + Function = 'function', + Api = 'api', + Other = '', +} + +export class ResourceNode implements TreeNode { + public readonly id = this.resourceTreeEntity.Id + private readonly type = this.resourceTreeEntity.Type + public readonly resourceLogicalId = this.deployedResource?.LogicalResourceId + + public constructor( + private readonly location: SamAppLocation, + private readonly resourceTreeEntity: ResourceTreeEntity, + private readonly stackName?: string, + private readonly region?: string, + private readonly deployedResource?: StackResource, + // TODO: cleanup or rename functionArn parameter as type can be differ from Lambda; value never set in generateResourceNodes() + private readonly functionArn?: string + ) {} + + public get resource() { + return { + resource: this.resourceTreeEntity, + location: this.location.samTemplateUri, + workspaceFolder: this.location.workspaceFolder, + region: this.region, + stackName: this.stackName, + deployedResource: this.deployedResource, + functionArn: this.functionArn, + } + } + + public async getChildren() { + let deployedNodes: DeployedResourceNode[] = [] + let propertyNodes: TreeNode[] = [] + + if (this.deployedResource && this.region && this.stackName) { + deployedNodes = await generateDeployedNode( + this.deployedResource, + this.region, + this.stackName, + this.resourceTreeEntity + ) + } + if (this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE) { + propertyNodes = generatePropertyNodes(this.resourceTreeEntity) + } + + return [...propertyNodes, ...deployedNodes] + } + + public getTreeItem(): vscode.TreeItem { + // Determine the initial TreeItem collapsible state based on the type + const collapsibleState = this.deployedResource + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + + // Create the TreeItem with the determined collapsible state + const item = new vscode.TreeItem(this.resourceTreeEntity.Id, collapsibleState) + + // Set the tooltip to the URI of the SAM template + item.tooltip = this.location.samTemplateUri.toString() + + item.iconPath = this.getIconPath() + + // Set the resource URI to the SAM template URI + item.resourceUri = this.location.samTemplateUri + + // Define the context value for the item + item.contextValue = `awsAppBuilderResourceNode.${this.getResourceId()}` + + return item + } + + // Additional resources and corresponding icons will be added in the future. + // When adding support for new resources, ensure that each new resource + // has an appropriate mapping in place. + private getIconPath(): IconPath | undefined { + switch (this.type) { + case SERVERLESS_FUNCTION_TYPE: + return getIcon('aws-lambda-function') + case s3BucketType: + return getIcon('aws-s3-bucket') + case appRunnerType: + return getIcon('aws-apprunner-service') + case ecrRepositoryType: + return getIcon('aws-ecr-registry') + default: + return getIcon('vscode-info') + } + } + + private getResourceId(): ResourceTypeId { + switch (this.type) { + case SERVERLESS_FUNCTION_TYPE: + return ResourceTypeId.Function + case 'Api': + return ResourceTypeId.Api + default: + return ResourceTypeId.Other + } + } +} + +export function generateResourceNodes( + app: SamAppLocation, + resources: NonNullable, + stackName?: string, + region?: string, + deployedResources?: StackResource[] +): ResourceNode[] { + if (!deployedResources) { + return resources.map((resource) => new ResourceNode(app, resource, stackName, region)) + } + + return resources.map((resource) => { + if (resource.Type) { + const deployedResource = deployedResources.find( + (deployedResource) => resource.Id === deployedResource.LogicalResourceId + ) + return new ResourceNode(app, resource, stackName, region, deployedResource) + } else { + return new ResourceNode(app, resource, stackName, region) + } + }) +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/rootNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/rootNode.ts new file mode 100644 index 00000000000..ce0406fd874 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/rootNode.ts @@ -0,0 +1,110 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { debugNewSamAppDocUrl } from '../../../../shared/constants' +import { telemetry } from '../../../../shared/telemetry/telemetry' +import { ResourceTreeDataProvider, TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +import { createPlaceholderItem } from '../../../../shared/treeview/utils' +import { localize, openUrl } from '../../../../shared/utilities/vsCodeUtils' +import { Commands } from '../../../../shared/vscode/commands2' +import { AppNode } from './appNode' +import { detectSamProjects } from '../detectSamProjects' +import globals from '../../../../shared/extensionGlobals' +import { WalkthroughNode } from './walkthroughNode' + +export async function getAppNodes(): Promise { + // no active workspace, show buttons in welcomeview + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + return [] + } + + const appsFound = await detectSamProjects() + + if (appsFound.length === 0) { + return [ + createPlaceholderItem( + localize('AWS.appBuilder.explorerNode.noApps', '[No IaC templates found in Workspaces]') + ), + ] + } + + const nodesToReturn: TreeNode[] = appsFound + .map((appLocation) => new AppNode(appLocation)) + .sort((a, b) => a.label.localeCompare(b.label) ?? 0) + + return nodesToReturn +} + +export class AppBuilderRootNode implements TreeNode { + public readonly id = 'appBuilder' + public readonly resource = this + private readonly onDidChangeChildrenEmitter = new vscode.EventEmitter() + public readonly onDidChangeChildren = this.onDidChangeChildrenEmitter.event + private readonly _refreshAppBuilderExplorer + private readonly _refreshAppBuilderForFileExplorer + + constructor() { + Commands.register('aws.appBuilder.viewDocs', () => { + void openUrl(debugNewSamAppDocUrl.toolkit) + telemetry.aws_help.emit({ name: 'appBuilder' }) + }) + this._refreshAppBuilderExplorer = (provider?: ResourceTreeDataProvider) => + Commands.register('aws.appBuilder.refresh', () => { + this.refresh() + if (provider) { + provider.refresh() + } + }) + + this._refreshAppBuilderForFileExplorer = (provider?: ResourceTreeDataProvider) => + Commands.register('aws.appBuilderForFileExplorer.refresh', () => { + this.refresh() + if (provider) { + provider.refresh() + } + }) + } + + public get refreshAppBuilderExplorer() { + return this._refreshAppBuilderExplorer + } + + public get refreshAppBuilderForFileExplorer() { + return this._refreshAppBuilderForFileExplorer + } + + public async getChildren() { + const nodesToReturn = await getAppNodes() + if (nodesToReturn.length === 0) { + return [] + } + + const walkthroughCompleted = globals.globalState.get('aws.toolkit.lambda.walkthroughCompleted') + // show walkthrough node if walkthrough not completed yet + if (!walkthroughCompleted) { + nodesToReturn.unshift(new WalkthroughNode()) + } + return nodesToReturn + } + + public refresh(): void { + this.onDidChangeChildrenEmitter.fire() + } + + public getTreeItem() { + const item = new vscode.TreeItem('APPLICATION BUILDER') + item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed + item.contextValue = 'awsAppBuilderRootNode' + + return item + } + + static #instance: AppBuilderRootNode + + static get instance(): AppBuilderRootNode { + return (this.#instance ??= new AppBuilderRootNode()) + } +} diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/walkthroughNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/walkthroughNode.ts new file mode 100644 index 00000000000..8f3432075df --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/walkthroughNode.ts @@ -0,0 +1,37 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +import { localize } from '../../../../shared/utilities/vsCodeUtils' + +/** + * Create Open Walkthrough Node in App builder sidebar + * + */ +export class WalkthroughNode implements TreeNode { + public readonly id = 'walkthrough' + public readonly resource: WalkthroughNode = this + + // Constructor left empty intentionally for future extensibility + public constructor() {} + + /** + * Generates the TreeItem for the Walkthrough Node. + * This item will appear in the sidebar with a label and command to open the walkthrough. + */ + public getTreeItem() { + const itemLabel = localize('AWS.appBuilder.openWalkthroughTitle', 'Walkthrough of Application Builder') + + const item = new vscode.TreeItem(itemLabel) + item.contextValue = 'awsWalkthroughNode' + item.command = { + title: localize('AWS.appBuilder.openWalkthroughTitle', 'Walkthrough of Application Builder'), + command: 'aws.toolkit.lambda.openWalkthrough', + } + + return item + } +} diff --git a/packages/core/src/awsService/appBuilder/explorer/openTemplate.ts b/packages/core/src/awsService/appBuilder/explorer/openTemplate.ts new file mode 100644 index 00000000000..686340719e3 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/openTemplate.ts @@ -0,0 +1,20 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegistry' +import { createTemplatePrompter, TemplateItem } from '../../../shared/sam/sync' +import { createExitPrompter } from '../../../shared/ui/common/exitPrompter' +import { Wizard } from '../../../shared/wizards/wizard' + +export interface OpenTemplateParams { + readonly template: TemplateItem +} + +export class OpenTemplateWizard extends Wizard { + public constructor(state: Partial, registry: CloudFormationTemplateRegistry) { + super({ initState: state, exitPrompterProvider: createExitPrompter }) + this.form.template.bindPrompter(() => createTemplatePrompter(registry)) + } +} diff --git a/packages/core/src/awsService/appBuilder/explorer/samProject.ts b/packages/core/src/awsService/appBuilder/explorer/samProject.ts new file mode 100644 index 00000000000..fdb4b8e2117 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/explorer/samProject.ts @@ -0,0 +1,103 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as CloudFormation from '../../../shared/cloudformation/cloudformation' +import { SamConfig, SamConfigErrorCode } from '../../../shared/sam/config' +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' +import { showViewLogsMessage } from '../../../shared/utilities/messages' + +export interface SamApp { + location: SamAppLocation + resourceTree: ResourceTreeEntity[] +} + +export interface SamAppLocation { + samTemplateUri: vscode.Uri + workspaceFolder: vscode.WorkspaceFolder + projectRoot: vscode.Uri +} + +export interface ResourceTreeEntity { + Id: string + Type: string + Runtime?: string + CodeUri?: string + Handler?: string + Events?: ResourceTreeEntity[] + Path?: string + Method?: string +} + +export async function getStackName(projectRoot: vscode.Uri): Promise { + try { + const samConfig = await SamConfig.fromProjectRoot(projectRoot) + const stackName = await samConfig.getCommandParam('global', 'stack_name') + const region = await samConfig.getCommandParam('global', 'region') + + return { stackName, region } + } catch (error: any) { + switch (error.code) { + case SamConfigErrorCode.samNoConfigFound: + getLogger().info('No stack name or region information available in samconfig.toml', error) + break + case SamConfigErrorCode.samConfigParseError: + getLogger().error(`Error getting stack name or region information: ${error.message}`, error) + void showViewLogsMessage('Encountered an issue reading samconfig.toml') + break + default: + getLogger().warn(`Error getting stack name or region information: ${error.message}`, error) + } + return {} + } +} + +export async function getApp(location: SamAppLocation): Promise { + const samTemplate = await CloudFormation.tryLoad(location.samTemplateUri) + if (!samTemplate.template) { + throw new ToolkitError(`Template at ${location.samTemplateUri.fsPath} is not valid`) + } + const templateResources = getResourceEntity(samTemplate.template) + + const resourceTree = [...templateResources] + + return { location, resourceTree } +} + +function getResourceEntity(template: any): ResourceTreeEntity[] { + const resourceTree: ResourceTreeEntity[] = [] + + for (const [logicalId, resource] of Object.entries(template?.Resources ?? []) as [string, any][]) { + const resourceEntity: ResourceTreeEntity = { + Id: logicalId, + Type: resource.Type, + Runtime: resource.Properties?.Runtime ?? template?.Globals?.Function?.Runtime, + Handler: resource.Properties?.Handler ?? template?.Globals?.Function?.Handler, + Events: resource.Properties?.Events ? getEvents(resource.Properties.Events) : undefined, + CodeUri: resource.Properties?.CodeUri ?? template?.Globals?.Function?.CodeUri, + } + resourceTree.push(resourceEntity) + } + + return resourceTree +} + +function getEvents(events: Record): ResourceTreeEntity[] { + const eventResources: ResourceTreeEntity[] = [] + + for (const [eventsLogicalId, event] of Object.entries(events)) { + const eventProperties = event.Properties + const eventResource: ResourceTreeEntity = { + Id: eventsLogicalId, + Type: event.Type, + Path: eventProperties.Path, + Method: eventProperties.Method, + } + eventResources.push(eventResource) + } + + return eventResources +} diff --git a/packages/core/src/awsService/appBuilder/utils.ts b/packages/core/src/awsService/appBuilder/utils.ts new file mode 100644 index 00000000000..de3dee8770d --- /dev/null +++ b/packages/core/src/awsService/appBuilder/utils.ts @@ -0,0 +1,198 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import * as nls from 'vscode-nls' +import { ResourceNode } from './explorer/nodes/resourceNode' +import type { SamAppLocation } from './explorer/samProject' +import { ToolkitError } from '../../shared/errors' +import globals from '../../shared/extensionGlobals' +import { OpenTemplateParams, OpenTemplateWizard } from './explorer/openTemplate' +import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter' +import { createCommonButtons } from '../../shared/ui/buttons' +import { samDeployUrl } from '../../shared/constants' +import path from 'path' +import fs from '../../shared/fs/fs' +import { getLogger } from '../../shared/logger/logger' +import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime' +import { showMessage } from '../../shared/utilities/messages' +const localize = nls.loadMessageBundle() + +export async function runOpenTemplate(arg?: TreeNode) { + const templateUri = arg ? (arg.resource as SamAppLocation).samTemplateUri : await promptUserForTemplate() + if (!templateUri || !(await fs.exists(templateUri))) { + throw new ToolkitError('No template provided', { code: 'NoTemplateProvided' }) + } + const document = await vscode.workspace.openTextDocument(templateUri) + await vscode.window.showTextDocument(document) +} + +/** + * Find and open the lambda handler with given ResoruceNode + * If not found, a NoHandlerFound error will be raised + * @param arg ResourceNode + */ +export async function runOpenHandler(arg: ResourceNode): Promise { + const folderUri = path.dirname(arg.resource.location.fsPath) + if (!arg.resource.resource.CodeUri) { + throw new ToolkitError('No CodeUri provided in template, cannot open handler', { code: 'NoCodeUriProvided' }) + } + + if (!arg.resource.resource.Handler) { + throw new ToolkitError('No Handler provided in template, cannot open handler', { code: 'NoHandlerProvided' }) + } + + if (!arg.resource.resource.Runtime) { + throw new ToolkitError('No Runtime provided in template, cannot open handler', { code: 'NoRuntimeProvided' }) + } + + const handlerFile = await getLambdaHandlerFile( + vscode.Uri.file(folderUri), + arg.resource.resource.CodeUri, + arg.resource.resource.Handler, + arg.resource.resource.Runtime + ) + if (!handlerFile) { + throw new ToolkitError(`No handler file found with name "${arg.resource.resource.Handler}"`, { + code: 'NoHandlerFound', + }) + } + await vscode.workspace.openTextDocument(handlerFile).then(async (doc) => await vscode.window.showTextDocument(doc)) +} + +// create a set to store all supported runtime in the following function +const supportedRuntimeForHandler = new Set([ + RuntimeFamily.Ruby, + RuntimeFamily.Python, + RuntimeFamily.NodeJS, + RuntimeFamily.DotNet, + RuntimeFamily.Java, +]) + +/** + * Get the actual Lambda handler file, in vscode.Uri format, from the template + * file and handler name. If not found, return undefined. + * + * @param folderUri The root folder for sam project + * @param codeUri codeUri prop in sam template + * @param handler handler prop in sam template + * @param runtime runtime prop in sam template + * @returns + */ +export async function getLambdaHandlerFile( + folderUri: vscode.Uri, + codeUri: string, + handler: string, + runtime: string +): Promise { + const family = getFamily(runtime) + if (!supportedRuntimeForHandler.has(family)) { + throw new ToolkitError(`Runtime ${runtime} is not supported for open handler button`, { + code: 'RuntimeNotSupported', + }) + } + + const handlerParts = handler.split('.') + // sample: app.lambda_handler -> app.rb + if (family === RuntimeFamily.Ruby) { + // Ruby supports namespace/class handlers as well, but the path is + // guaranteed to be slash-delimited so we can assume the first part is + // the path + return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.rb') + } + + // sample:app.lambda_handler -> app.py + if (family === RuntimeFamily.Python) { + // Otherwise (currently Node.js and Python) handle dot-delimited paths + return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.py') + } + + // sample: app.handler -> app.mjs/app.js + // More likely to be mjs if NODEJS version>=18, now searching for both + if (family === RuntimeFamily.NodeJS) { + const handlerName = handlerParts.slice(0, handlerParts.length - 1).join('/') + const handlerPath = path.dirname(handlerName) + const handlerFile = path.basename(handlerName) + const pattern = new vscode.RelativePattern( + vscode.Uri.joinPath(folderUri, codeUri, handlerPath), + `${handlerFile}.{js,mjs}` + ) + return searchHandlerFile(folderUri, pattern) + } + // search directly under Code uri for Dotnet and java + // sample: ImageResize::ImageResize.Function::FunctionHandler -> Function.cs + if (family === RuntimeFamily.DotNet) { + const handlerName = path.basename(handler.split('::')[1].replaceAll('.', '/')) + const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `${handlerName}.cs`) + return searchHandlerFile(folderUri, pattern) + } + + // sample: resizer.App::handleRequest -> App.java + if (family === RuntimeFamily.Java) { + const handlerName = handler.split('::')[0].replaceAll('.', '/') + const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `**/${handlerName}.java`) + return searchHandlerFile(folderUri, pattern) + } +} + +/** + Searches for a handler file in the given pattern and returns the first match. + If no match is found, returns undefined. +*/ +export async function searchHandlerFile( + folderUri: vscode.Uri, + pattern: vscode.RelativePattern +): Promise { + const handlerFile = await vscode.workspace.findFiles(pattern, new vscode.RelativePattern(folderUri, '.aws-sam')) + if (handlerFile.length === 0) { + return undefined + } + if (handlerFile.length > 1) { + getLogger().warn(`Multiple handler files found with name "${path.basename(handlerFile[0].fsPath)}"`) + void showMessage('warn', `Multiple handler files found with name "${path.basename(handlerFile[0].fsPath)}"`) + } + if (await fs.exists(handlerFile[0])) { + return handlerFile[0] + } + return undefined +} + +async function promptUserForTemplate() { + const registry = await globals.templateRegistry + const openTemplateParams: Partial = {} + + const param = await new OpenTemplateWizard(openTemplateParams, registry).run() + return param?.template.uri +} + +export async function deployTypePrompt() { + const items: DataQuickPickItem[] = [ + { + label: 'Sync', + data: 'sync', + detail: 'Speed up your development and testing experience in the AWS Cloud. With the --watch parameter, sync will build, deploy and watch for local changes', + description: 'Development environments', + }, + { + label: 'Deploy', + data: 'deploy', + detail: 'Deploys your template through CloudFormation', + description: 'Production environments', + }, + ] + + const selected = await createQuickPick(items, { + title: localize('AWS.appBuilder.deployType.title', 'Select deployment command'), + placeholder: 'Press enter to proceed with highlighted option', + buttons: createCommonButtons(samDeployUrl), + }).prompt() + + if (!selected) { + getLogger().info('Operation cancelled.') + return + } + return selected +} diff --git a/packages/core/src/awsService/appBuilder/walkthrough.ts b/packages/core/src/awsService/appBuilder/walkthrough.ts new file mode 100644 index 00000000000..04f43d61878 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/walkthrough.ts @@ -0,0 +1,349 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as semver from 'semver' +import * as vscode from 'vscode' +import globals from '../../shared/extensionGlobals' +import { getLogger } from '../../shared/logger' + +import { Wizard } from '../../shared/wizards/wizard' +import { createQuickPick } from '../../shared/ui/pickerPrompter' +import { createCommonButtons } from '../../shared/ui/buttons' +import * as nls from 'vscode-nls' +import { ToolkitError } from '../../shared/errors' +import { createSingleFileDialog } from '../../shared/ui/common/openDialog' +import { fs } from '../../shared/fs/fs' +import path from 'path' +import { telemetry } from '../../shared/telemetry' + +import { minSamCliVersionForAppBuilderSupport } from '../../shared/sam/cli/samCliValidator' +import { SamCliInfoInvocation } from '../../shared/sam/cli/samCliInfo' +import { openUrl } from '../../shared/utilities/vsCodeUtils' +import { getOrInstallCli, awsClis, AwsClis } from '../../shared/utilities/cliUtils' +import { getPattern } from '../../shared/utilities/downloadPatterns' +import { addFolderToWorkspace } from '../../shared/utilities/workspaceUtils' + +const localize = nls.loadMessageBundle() +const serverlessLandUrl = 'https://serverlessland.com/' +export const walkthroughContextString = 'aws.toolkit.lambda.walkthroughSelected' +export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpenOnStart' +const defaultTemplateName = 'template.yaml' +const serverlessLandOwner = 'aws-samples' +const serverlessLandRepo = 'serverless-patterns' + +type WalkthroughOptions = 'CustomTemplate' | 'Visual' | 'S3' | 'API' +type TutorialRuntimeOptions = 'python' | 'node' | 'java' | 'dotnet' | 'skipped' + +interface IServerlessLandProject { + asset: string + handler?: string +} + +export const appMap = new Map([ + ['APIdotnet', { asset: 'apigw-rest-api-lambda-dotnet.zip', handler: 'src/HelloWorld/Function.cs' }], + ['APInode', { asset: 'apigw-rest-api-lambda-node.zip', handler: 'hello_world/app.mjs' }], + ['APIpython', { asset: 'apigw-rest-api-lambda-python.zip', handler: 'hello_world/app.py' }], + [ + 'APIjava', + { + asset: 'apigw-rest-api-lambda-java.zip', + handler: 'HelloWorldFunction/src/main/java/helloworld/App.java', + }, + ], + ['S3dotnet', { asset: 's3-lambda-resizing-dotnet.zip', handler: 'ImageResize/Function.cs' }], + ['S3node', { asset: 's3-lambda-resizing-node.zip', handler: 'src/app.js' }], + ['S3python', { asset: 's3-lambda-resizing-python.zip', handler: 'src/app.py' }], + [ + 'S3java', + { + asset: 's3-lambda-resizing-java.zip', + handler: 'ResizerFunction/src/main/java/resizer/App.java', + }, + ], +]) + +export class RuntimeLocationWizard extends Wizard<{ + runtime: TutorialRuntimeOptions + dir: string + realDir: vscode.Uri +}> { + public constructor(skipRuntime: boolean, labelValue: string, existingTemplates?: vscode.Uri[]) { + super() + const form = this.form + + // step1: choose runtime + const items: { label: string; data: TutorialRuntimeOptions }[] = [ + { label: 'Python', data: 'python' }, + { label: 'Node JS', data: 'node' }, + { label: 'Java', data: 'java' }, + { label: 'Dot Net', data: 'dotnet' }, + ] + form.runtime.bindPrompter( + () => { + return createQuickPick(items, { + title: localize('AWS.toolkit.lambda.walkthroughSelectRuntime', 'Select a runtime'), + buttons: createCommonButtons(serverlessLandUrl), + }) + }, + { showWhen: () => !skipRuntime } + ) + + // step2: choose location for project + const wsFolders = vscode.workspace.workspaceFolders + const items2 = [ + { + label: localize('AWS.toolkit.lambda.walkthroughOpenExplorer', 'Open file explorer'), + data: 'file-selector', + }, + ] + + // if at least one open workspace, add all opened workspace as options + if (wsFolders && labelValue !== 'Open existing Project') { + for (const wsFolder of wsFolders) { + items2.push({ label: wsFolder.uri.fsPath, data: wsFolder.uri.fsPath }) + } + } + + if (wsFolders && existingTemplates && labelValue === 'Open existing Project') { + existingTemplates.map((file) => { + items2.push({ label: file.fsPath, data: path.dirname(file.fsPath) }) + }) + } + + form.dir.bindPrompter(() => { + return createQuickPick(items2, { + title: + labelValue === 'Open existing Project' + ? localize('AWS.toolkit.lambda.walkthroughOpenExistProject', 'Select an existing template file') + : localize('AWS.toolkit.lambda.walkthroughProjectLocation', 'Select a location for project'), + buttons: createCommonButtons(labelValue === 'Open existing Project' ? undefined : serverlessLandUrl), + }) + }) + + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: labelValue, + canSelectFiles: false, + canSelectFolders: true, + } + if (wsFolders) { + options.defaultUri = wsFolders[0]?.uri + } + + form.realDir.bindPrompter((state) => createSingleFileDialog(options), { + showWhen: (state) => state.dir !== undefined && state.dir === 'file-selector', + setDefault: (state) => (state.dir ? vscode.Uri.file(state.dir) : undefined), + }) + } +} + +export async function getTutorial( + runtime: TutorialRuntimeOptions, + project: WalkthroughOptions, + outputDir: vscode.Uri, + source?: string +): Promise { + const appSelected = appMap.get(project + runtime) + telemetry.record({ action: project + runtime, source: source ?? 'AppBuilderWalkthrough' }) + if (!appSelected) { + throw new ToolkitError(`Tried to get template '${project}+${runtime}', but it hasn't been registered.`) + } + + try { + await getPattern(serverlessLandOwner, serverlessLandRepo, appSelected.asset, outputDir, true) + } catch (error) { + throw new ToolkitError(`Error occurred while fetching the pattern from serverlessland: ${error}`) + } +} + +/** + * Takes projectUri and runtime then generate matching project + * @param walkthroughSelected the selected walkthrough + * @param projectUri The choosen project uri to generate proejct + * @param runtime The runtime choosen + */ +export async function genWalkthroughProject( + walkthroughSelected: WalkthroughOptions, + projectUri: vscode.Uri, + runtime: TutorialRuntimeOptions | undefined +): Promise { + // create project here + // TODO update with file fetching from serverless land + if (walkthroughSelected === 'CustomTemplate') { + // customer already have a project, no need to generate + return + } + + // check if template.yaml already exists + const templateUri = vscode.Uri.joinPath(projectUri, defaultTemplateName) + if (await fs.exists(templateUri)) { + // ask if want to overwrite + const choice = await vscode.window.showInformationMessage( + localize( + 'AWS.toolkit.lambda.walkthroughCreateProjectPrompt', + '{0} already exist in the selected directory, overwrite?', + defaultTemplateName + ), + 'Yes', + 'No' + ) + if (choice !== 'Yes') { + throw new ToolkitError(`${defaultTemplateName} already exist`) + } + } + + // if Yes, or template not found, continue to generate + if (walkthroughSelected === 'Visual') { + // create an empty template.yaml, open it in appcomposer later + await fs.writeFile(templateUri, Buffer.from('')) + return + } + // start fetching project + if (runtime && runtime !== 'skipped') { + await getTutorial(runtime, walkthroughSelected, projectUri, 'AppBuilderWalkthrough') + } +} + +/** + * check if the selected project Uri exist in current workspace. If not, add Project folder to Workspace + * @param projectUri uri for the selected project + */ +export async function openProjectInWorkspace(projectUri: vscode.Uri): Promise { + let templateUri: vscode.Uri | undefined = vscode.Uri.joinPath(projectUri, defaultTemplateName) + if (!(await fs.exists(templateUri))) { + // no template.yaml, trying yml + templateUri = vscode.Uri.joinPath(projectUri, 'template.yml') + if (!(await fs.exists(templateUri))) { + templateUri = undefined + } + } + + // Open template file, and after update workspace folder + if (templateUri) { + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') + await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(templateUri)) + // set global key to template to be opened, appComposer will open them upon reload + await globals.globalState.update(templateToOpenAppComposer, [templateUri.fsPath]) + } + + // if extension is reloaded here, this function will never return (killed) + await addFolderToWorkspace({ uri: projectUri, name: path.basename(projectUri.fsPath) }, true) + + // Open template file + if (templateUri) { + // extension not reloaded, set to false + await globals.globalState.update(templateToOpenAppComposer, undefined) + await vscode.commands.executeCommand('aws.openInApplicationComposer', templateUri) + } + + // Open Readme if exist + if (await fs.exists(vscode.Uri.joinPath(projectUri, 'README.md'))) { + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') + await vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.joinPath(projectUri, 'README.md')) + } +} + +/** + * Used in Toolkit Appbuilder Walkthrough. + * 1: Customer select a template + * 2: Create project / Or don't create if customer choose use my own template + * 3: Add project to workspace, Open template.yaml, open template.yaml in AppComposer + */ +export async function initWalkthroughProjectCommand() { + const walkthroughSelected = globals.globalState.get(walkthroughContextString) + let runtimeSelected: TutorialRuntimeOptions | undefined = undefined + try { + if (!walkthroughSelected || !(typeof walkthroughSelected === 'string')) { + getLogger().info('exit on no walkthrough selected') + void vscode.window.showErrorMessage( + localize('AWS.toolkit.lambda.walkthroughNotSelected', 'Please select a template first') + ) + return + } + let labelValue = 'Create Project' + if (walkthroughSelected === 'CustomTemplate') { + labelValue = 'Open existing Project' + } + // if these two, skip runtime choice + const skipRuntimeChoice = walkthroughSelected === 'Visual' || walkthroughSelected === 'CustomTemplate' + const templates: vscode.Uri[] = + walkthroughSelected === 'CustomTemplate' + ? await vscode.workspace.findFiles('**/{template.yaml,template.yml}', '**/.aws-sam/*') + : [] + const result = await new RuntimeLocationWizard(skipRuntimeChoice, labelValue, templates).run() + if (!result) { + getLogger().info('User canceled the runtime selection process via quickpick') + return + } + + if (!result.realDir || !fs.exists(result.realDir)) { + // exit for non-vaild uri + getLogger().info('exit on customer fileselector cancellation') + return + } + + runtimeSelected = result.runtime + + // generate project + await genWalkthroughProject(walkthroughSelected, result.realDir, runtimeSelected) + // open a workspace if no workspace yet + await openProjectInWorkspace(result.realDir) + } finally { + telemetry.record({ action: `${walkthroughSelected}:${runtimeSelected}`, source: 'AppBuilderWalkthrough' }) + } +} + +export async function getOrUpdateOrInstallSAMCli(source: string) { + try { + // find sam + const samPath = await getOrInstallCli('sam-cli', true, true) + // check version + const samCliVersion = (await new SamCliInfoInvocation(samPath).execute()).version + + if (semver.lt(samCliVersion, minSamCliVersionForAppBuilderSupport)) { + // sam found but version too low + const updateInstruction = localize( + 'AWS.toolkit.updateSAMInstruction', + 'View AWS SAM CLI update instructions' + ) + const selection = await vscode.window.showInformationMessage( + localize( + 'AWS.toolkit.samOutdatedPrompt', + 'AWS SAM CLI version {0} or greater is required ({1} currently installed).', + minSamCliVersionForAppBuilderSupport, + samCliVersion + ), + updateInstruction + ) + if (selection === updateInstruction) { + void openUrl(vscode.Uri.parse(awsClis['sam-cli'].manualInstallLink)) + } + } + } catch (err) { + throw ToolkitError.chain(err, 'Failed to install or detect SAM') + } finally { + telemetry.record({ source: source, toolId: 'sam-cli' }) + } +} + +/** + * wraps getOrinstallCli and send telemetry + * @param toolId to install/check + * @param source to be added in telemetry + */ +export async function getOrInstallCliWrapper(toolId: AwsClis, source: string) { + await telemetry.appBuilder_installTool.run(async (span) => { + span.record({ source: source, toolId: toolId }) + if (toolId === 'sam-cli') { + await getOrUpdateOrInstallSAMCli(source) + return + } + try { + await getOrInstallCli(toolId, true, true) + } finally { + telemetry.record({ source: source, toolId: toolId }) + } + }) +} diff --git a/packages/core/src/awsService/appBuilder/wizards/deployTypeWizard.ts b/packages/core/src/awsService/appBuilder/wizards/deployTypeWizard.ts new file mode 100644 index 00000000000..fbaec4657ca --- /dev/null +++ b/packages/core/src/awsService/appBuilder/wizards/deployTypeWizard.ts @@ -0,0 +1,54 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { samDeployUrl } from '../../../shared/constants' +import { createCommonButtons } from '../../../shared/ui/buttons' +import { DataQuickPickItem, createQuickPick } from '../../../shared/ui/pickerPrompter' +import * as nls from 'vscode-nls' +import { Wizard } from '../../../shared/wizards/wizard' +import { DeployParams, DeployWizard } from '../../../shared/sam/deploy' +import { SyncParams, SyncWizard } from '../../../shared/sam/sync' +import { WizardPrompter } from '../../../shared/ui/wizardPrompter' +import { createExitPrompter } from '../../../shared/ui/common/exitPrompter' +const localize = nls.loadMessageBundle() + +export class DeployTypeWizard extends Wizard<{ + choice: string + syncParam: SyncParams + deployParam: DeployParams +}> { + public constructor(syncWizard: SyncWizard, deployWizard: DeployWizard) { + super({ exitPrompterProvider: createExitPrompter }) + const form = this.form + + const items: DataQuickPickItem[] = [ + { + label: 'Sync', + data: 'sync', + detail: 'Speed up your development and testing experience in the AWS Cloud. With the --watch parameter, sync will build, deploy and watch for local changes', + description: 'Development environments', + }, + { + label: 'Deploy', + data: 'deploy', + detail: 'Deploys your template through CloudFormation', + description: 'Production environments', + }, + ] + form.choice.bindPrompter(() => { + return createQuickPick(items, { + title: localize('AWS.appBuilder.deployType.title', 'Select deployment command'), + placeholder: 'Press enter to proceed with highlighted option', + buttons: createCommonButtons(samDeployUrl), + }) + }) + form.deployParam.bindPrompter((state) => new WizardPrompter(deployWizard), { + showWhen: (state) => state.choice === 'deploy', + }) + form.syncParam.bindPrompter((state) => new WizardPrompter(syncWizard), { + showWhen: (state) => state.choice === 'sync', + }) + } +} diff --git a/packages/core/src/awsService/cloudWatchLogs/activation.ts b/packages/core/src/awsService/cloudWatchLogs/activation.ts index a186a8ba983..5c3f4ac91c8 100644 --- a/packages/core/src/awsService/cloudWatchLogs/activation.ts +++ b/packages/core/src/awsService/cloudWatchLogs/activation.ts @@ -19,6 +19,10 @@ import { searchLogGroup } from './commands/searchLogGroup' import { changeLogSearchParams } from './changeLogSearch' import { CloudWatchLogsNode } from './explorer/cloudWatchLogsNode' import { loadAndOpenInitialLogStreamFile, LogStreamCodeLensProvider } from './document/logStreamsCodeLensProvider' +import { DeployedResourceNode } from '../appBuilder/explorer/nodes/deployedNode' +import { isTreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { getLogger } from '../../shared/logger/logger' +import { ToolkitError } from '../../shared' export async function activate(context: vscode.ExtensionContext, configuration: Settings): Promise { const registry = LogDataRegistry.instance @@ -89,6 +93,26 @@ export async function activate(context: vscode.ExtensionContext, configuration: Commands.register('aws.cwl.changeFilterPattern', async () => changeLogSearchParams(registry, 'filterPattern')), - Commands.register('aws.cwl.changeTimeFilter', async () => changeLogSearchParams(registry, 'timeFilter')) + Commands.register('aws.cwl.changeTimeFilter', async () => changeLogSearchParams(registry, 'timeFilter')), + + Commands.register('aws.appBuilder.searchLogs', async (node: DeployedResourceNode) => { + try { + const logGroupInfo = isTreeNode(node) + ? { + regionName: node.resource.regionCode, + groupName: getFunctionLogGroupName(node.resource.explorerNode.configuration), + } + : undefined + const source: string = logGroupInfo ? 'AppBuilderSearchLogs' : 'CommandPaletteSearchLogs' + await searchLogGroup(registry, source, logGroupInfo) + } catch (err) { + getLogger().error('Failed to search logs: %s', err) + throw ToolkitError.chain(err, 'Failed to search logs') + } + }) ) } +function getFunctionLogGroupName(configuration: any) { + const logGroupPrefix = '/aws/lambda/' + return configuration.logGroupName || logGroupPrefix + configuration.FunctionName +} diff --git a/packages/core/src/awsService/s3/activation.ts b/packages/core/src/awsService/s3/activation.ts index fcaee4f3905..2b8a164800a 100644 --- a/packages/core/src/awsService/s3/activation.ts +++ b/packages/core/src/awsService/s3/activation.ts @@ -25,6 +25,8 @@ import { Commands } from '../../shared/vscode/commands2' import * as nls from 'vscode-nls' import { DefaultS3Client } from '../../shared/clients/s3Client' +import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { getSourceNode } from '../../shared/utilities/treeNodeUtils' const localize = nls.loadMessageBundle() /** @@ -58,7 +60,7 @@ export async function activate(ctx: ExtContext): Promise { }), Commands.register( { id: 'aws.s3.uploadFile', autoconnect: true }, - async (node?: S3BucketNode | S3FolderNode) => { + async (node?: S3BucketNode | S3FolderNode | TreeNode) => { if (!node) { const awsContext = ctx.awsContext const regionCode = awsContext.getCredentialDefaultRegion() @@ -66,7 +68,8 @@ export async function activate(ctx: ExtContext): Promise { const document = vscode.window.activeTextEditor?.document.uri await uploadFileCommand(s3Client, document) } else { - await uploadFileCommand(node.s3, node) + const sourceNode = getSourceNode(node) + await uploadFileCommand(sourceNode.s3, sourceNode) } } ), @@ -76,11 +79,13 @@ export async function activate(ctx: ExtContext): Promise { Commands.register('aws.s3.createBucket', async (node: S3Node) => { await createBucketCommand(node) }), - Commands.register('aws.s3.createFolder', async (node: S3BucketNode | S3FolderNode) => { - await createFolderCommand(node) + Commands.register('aws.s3.createFolder', async (node: S3BucketNode | S3FolderNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await createFolderCommand(sourceNode) }), - Commands.register('aws.s3.deleteBucket', async (node: S3BucketNode) => { - await deleteBucketCommand(node) + Commands.register('aws.s3.deleteBucket', async (node: S3BucketNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await deleteBucketCommand(sourceNode) }), Commands.register('aws.s3.deleteFile', async (node: S3FileNode) => { await deleteFileCommand(node) diff --git a/packages/core/src/awsexplorer/activation.ts b/packages/core/src/awsexplorer/activation.ts index a93baef6034..5ea7295bf98 100644 --- a/packages/core/src/awsexplorer/activation.ts +++ b/packages/core/src/awsexplorer/activation.ts @@ -32,6 +32,10 @@ import { activateViewsShared, registerToolView } from './activationShared' import { isExtensionInstalled } from '../shared/utilities' import { CommonAuthViewProvider } from '../login/webview' import { setContext } from '../shared' +import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' +import { getSourceNode } from '../shared/utilities/treeNodeUtils' +import { openAwsCFNConsoleCommand, openAwsConsoleCommand } from '../shared/awsConsole' +import { StackNameNode } from '../awsService/appBuilder/explorer/nodes/deployedStack' /** * Activates the AWS Explorer UI and related functionality. @@ -121,6 +125,7 @@ export async function activate(args: { refreshCommands: [refreshAmazonQ, refreshAmazonQRootNode], }) } + const viewNodes: ToolView[] = [ ...amazonQViewNode, ...codecatalystViewNode, @@ -196,8 +201,21 @@ async function registerAwsExplorerCommands( isPreviewAndRender: true, }) ), - Commands.register('aws.copyArn', async (node: AWSResourceNode) => await copyTextCommand(node, 'ARN')), - Commands.register('aws.copyName', async (node: AWSResourceNode) => await copyTextCommand(node, 'name')), + Commands.register('aws.copyArn', async (node: AWSResourceNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await copyTextCommand(sourceNode, 'ARN') + }), + Commands.register('aws.copyName', async (node: AWSResourceNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await copyTextCommand(sourceNode, 'name') + }), + Commands.register('aws.openAwsConsole', async (node: AWSResourceNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await openAwsConsoleCommand(sourceNode) + }), + Commands.register('aws.openAwsCFNConsole', async (node: StackNameNode) => { + await openAwsCFNConsoleCommand(node) + }), Commands.register('aws.refreshAwsExplorerNode', async (element: AWSTreeNodeBase | undefined) => { awsExplorer.refresh(element) }), diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts index 98f222578c8..42aab7bcf09 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' import * as codecatalyst from './codecatalyst/activation' +import { activate as activateAppBuilder } from './awsService/appBuilder/activation' import { activate as activateAwsExplorer } from './awsexplorer/activation' import { activate as activateCloudWatchLogs } from './awsService/cloudWatchLogs/activation' import { activate as activateSchemas } from './eventSchemas/activation' @@ -203,6 +204,8 @@ export async function activate(context: vscode.ExtensionContext) { await activateRedshift(extContext) + await activateAppBuilder(extContext) + await activateIamPolicyChecks(extContext) context.subscriptions.push( diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts index 911f3f9be44..4a21b2e9611 100644 --- a/packages/core/src/lambda/activation.ts +++ b/packages/core/src/lambda/activation.ts @@ -11,25 +11,36 @@ import { downloadLambdaCommand } from './commands/downloadLambda' import { tryRemoveFolder } from '../shared/filesystemUtilities' import { ExtContext } from '../shared/extensions' import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda' -import { registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend' +import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend' import { Commands } from '../shared/vscode/commands2' import { DefaultLambdaClient } from '../shared/clients/lambdaClient' import { copyLambdaUrl } from './commands/copyLambdaUrl' +import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode' +import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider' +import { getSourceNode } from '../shared/utilities/treeNodeUtils' /** * Activates Lambda components. */ export async function activate(context: ExtContext): Promise { context.extensionContext.subscriptions.push( - Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode) => { - await deleteLambda(node.configuration, new DefaultLambdaClient(node.regionCode)) - await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', node.parent) + Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await deleteLambda(sourceNode.configuration, new DefaultLambdaClient(sourceNode.regionCode)) + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', sourceNode.parent) + }), + Commands.register('aws.invokeLambda', async (node: LambdaFunctionNode | TreeNode) => { + let source: string = 'AwsExplorerRemoteInvoke' + if (isTreeNode(node)) { + node = getSourceNode(node) + source = 'AppBuilderRemoteInvoke' + } + await invokeRemoteLambda(context, { + outputChannel: context.outputChannel, + functionNode: node, + source: source, + }) }), - Commands.register( - 'aws.invokeLambda', - async (node: LambdaFunctionNode) => - await invokeRemoteLambda(context, { outputChannel: context.outputChannel, functionNode: node }) - ), // Capture debug finished events, and delete the temporary directory if it exists vscode.debug.onDidTerminateDebugSession(async (session) => { if ( @@ -39,7 +50,10 @@ export async function activate(context: ExtContext): Promise { await tryRemoveFolder(session.configuration.baseBuildDir) } }), - Commands.register('aws.downloadLambda', async (node: LambdaFunctionNode) => await downloadLambdaCommand(node)), + Commands.register('aws.downloadLambda', async (node: LambdaFunctionNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await downloadLambdaCommand(sourceNode) + }), Commands.register({ id: 'aws.uploadLambda', autoconnect: true }, async (arg?: unknown) => { if (arg instanceof LambdaFunctionNode) { await uploadLambdaCommand({ @@ -53,10 +67,15 @@ export async function activate(context: ExtContext): Promise { await uploadLambdaCommand() } }), - Commands.register( - 'aws.copyLambdaUrl', - async (node: LambdaFunctionNode) => await copyLambdaUrl(node, new DefaultLambdaClient(node.regionCode)) - ), - registerSamInvokeVueCommand(context) + Commands.register('aws.copyLambdaUrl', async (node: LambdaFunctionNode | TreeNode) => { + const sourceNode = getSourceNode(node) + await copyLambdaUrl(sourceNode, new DefaultLambdaClient(sourceNode.regionCode)) + }), + + registerSamInvokeVueCommand(context), + + Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) => + registerSamDebugInvokeVueCommand(context, { resource: node }) + ) ) } diff --git a/packages/core/src/lambda/commands/deploySamApplication.ts b/packages/core/src/lambda/commands/deploySamApplication.ts deleted file mode 100644 index 4231b2b614e..00000000000 --- a/packages/core/src/lambda/commands/deploySamApplication.ts +++ /dev/null @@ -1,326 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as path from 'path' -import * as vscode from 'vscode' -import * as nls from 'vscode-nls' - -import { asEnvironmentVariables } from '../../auth/credentials/utils' -import { AwsContext } from '../../shared/awsContext' -import globals from '../../shared/extensionGlobals' - -import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' -import { checklogs } from '../../shared/localizedText' -import { getLogger } from '../../shared/logger' -import { SamCliBuildInvocation } from '../../shared/sam/cli/samCliBuild' -import { SamCliSettings } from '../../shared/sam/cli/samCliSettings' -import { getSamCliContext, SamCliContext, getSamCliVersion } from '../../shared/sam/cli/samCliContext' -import { runSamCliDeploy } from '../../shared/sam/cli/samCliDeploy' -import { SamCliProcessInvoker } from '../../shared/sam/cli/samCliInvokerUtils' -import { runSamCliPackage } from '../../shared/sam/cli/samCliPackage' -import { throwAndNotifyIfInvalid } from '../../shared/sam/cli/samCliValidationUtils' -import { Result } from '../../shared/telemetry/telemetry' -import { addCodiconToString } from '../../shared/utilities/textUtilities' -import { SamDeployWizardResponse } from '../wizards/samDeployWizard' -import { telemetry } from '../../shared/telemetry/telemetry' - -const localize = nls.loadMessageBundle() - -interface DeploySamApplicationParameters { - sourceTemplatePath: string - deployRootFolder: string - environmentVariables: NodeJS.ProcessEnv - region: string - packageBucketName: string - ecrRepo?: string - destinationStackName: string - parameterOverrides: Map -} - -export interface WindowFunctions { - showInformationMessage: typeof vscode.window.showInformationMessage - showErrorMessage: typeof vscode.window.showErrorMessage - setStatusBarMessage(text: string, hideWhenDone: Thenable): vscode.Disposable -} - -export async function deploySamApplication( - { - samCliContext = getSamCliContext(), - samDeployWizard, - }: { - samCliContext?: SamCliContext - samDeployWizard: () => Promise - }, - { - awsContext, - settings, - window = getDefaultWindowFunctions(), - refreshFn = () => { - // no need to await, doesn't need to block further execution (true -> no telemetry) - void vscode.commands.executeCommand('aws.refreshAwsExplorer', true) - }, - }: { - awsContext: Pick - settings: SamCliSettings - window?: WindowFunctions - refreshFn?: () => void - } -): Promise { - let deployResult: Result = 'Succeeded' - let samVersion: string | undefined - let deployFolder: string | undefined - try { - const credentials = await awsContext.getCredentials() - if (!credentials) { - throw new Error('No AWS profile selected') - } - - throwAndNotifyIfInvalid(await samCliContext.validator.detectValidSamCli()) - - const deployWizardResponse = await samDeployWizard() - - if (!deployWizardResponse) { - return - } - - deployFolder = await makeTemporaryToolkitFolder('samDeploy') - samVersion = await getSamCliVersion(samCliContext) - - const deployParameters: DeploySamApplicationParameters = { - deployRootFolder: deployFolder, - destinationStackName: deployWizardResponse.stackName, - packageBucketName: deployWizardResponse.s3Bucket, - ecrRepo: deployWizardResponse.ecrRepo?.repositoryUri, - parameterOverrides: deployWizardResponse.parameterOverrides, - environmentVariables: asEnvironmentVariables(credentials), - region: deployWizardResponse.region, - sourceTemplatePath: deployWizardResponse.template.fsPath, - } - - const deployApplicationPromise = deploy({ - deployParameters, - invoker: samCliContext.invoker, - window, - }) - - window.setStatusBarMessage( - addCodiconToString( - 'cloud-upload', - localize( - 'AWS.samcli.deploy.statusbar.message', - 'Deploying SAM Application to {0}...', - deployWizardResponse.stackName - ) - ), - deployApplicationPromise - ) - - await deployApplicationPromise - refreshFn() - - // successful deploy: retain S3 bucket for quick future access - const profile = awsContext.getCredentialProfileName() - if (profile) { - await settings.updateSavedBuckets(profile, deployWizardResponse.region, deployWizardResponse.s3Bucket) - } else { - getLogger().warn('Profile not provided; cannot write recent buckets.') - } - } catch (err) { - deployResult = 'Failed' - outputDeployError(err as Error) - void vscode.window.showErrorMessage( - localize('AWS.samcli.deploy.workflow.error', 'Failed to deploy SAM application.') - ) - } finally { - await tryRemoveFolder(deployFolder) - telemetry.sam_deploy.emit({ result: deployResult, version: samVersion }) - } -} - -function getBuildRootFolder(deployRootFolder: string): string { - return path.join(deployRootFolder, 'build') -} - -function getBuildTemplatePath(deployRootFolder: string): string { - // Assumption: sam build will always produce a template.yaml file. - // If that is not the case, revisit this logic. - return path.join(getBuildRootFolder(deployRootFolder), 'template.yaml') -} - -function getPackageTemplatePath(deployRootFolder: string): string { - return path.join(deployRootFolder, 'template.yaml') -} - -async function buildOperation(params: { - deployParameters: DeploySamApplicationParameters - invoker: SamCliProcessInvoker -}): Promise { - try { - getLogger().info(localize('AWS.samcli.deploy.workflow.init', 'Building SAM Application...')) - - const buildDestination = getBuildRootFolder(params.deployParameters.deployRootFolder) - - const build = new SamCliBuildInvocation({ - buildDir: buildDestination, - baseDir: undefined, - templatePath: params.deployParameters.sourceTemplatePath, - invoker: params.invoker, - }) - - await build.execute() - - return true - } catch (err) { - getLogger().warn( - localize( - 'AWS.samcli.build.failedBuild', - '"sam build" failed: {0}', - params.deployParameters.sourceTemplatePath - ) - ) - return false - } -} - -async function packageOperation( - params: { - deployParameters: DeploySamApplicationParameters - invoker: SamCliProcessInvoker - }, - buildSuccessful: boolean -): Promise { - if (!buildSuccessful) { - void vscode.window.showInformationMessage( - localize( - 'AWS.samcli.deploy.workflow.packaging.noBuild', - 'Attempting to package source template directory directly since "sam build" failed' - ) - ) - } - - getLogger().info( - localize( - 'AWS.samcli.deploy.workflow.packaging', - 'Packaging SAM Application to S3 Bucket: {0}', - params.deployParameters.packageBucketName - ) - ) - - // HACK: Attempt to package the initial template if the build fails. - const buildTemplatePath = buildSuccessful - ? getBuildTemplatePath(params.deployParameters.deployRootFolder) - : params.deployParameters.sourceTemplatePath - const packageTemplatePath = getPackageTemplatePath(params.deployParameters.deployRootFolder) - - await runSamCliPackage( - { - sourceTemplateFile: buildTemplatePath, - destinationTemplateFile: packageTemplatePath, - environmentVariables: params.deployParameters.environmentVariables, - region: params.deployParameters.region, - s3Bucket: params.deployParameters.packageBucketName, - ecrRepo: params.deployParameters.ecrRepo, - }, - params.invoker - ) -} - -async function deployOperation(params: { - deployParameters: DeploySamApplicationParameters - invoker: SamCliProcessInvoker -}): Promise { - try { - getLogger().info( - localize( - 'AWS.samcli.deploy.workflow.stackName.initiated', - 'Deploying SAM Application to CloudFormation Stack: {0}', - params.deployParameters.destinationStackName - ) - ) - - const packageTemplatePath = getPackageTemplatePath(params.deployParameters.deployRootFolder) - - await runSamCliDeploy( - { - parameterOverrides: params.deployParameters.parameterOverrides, - environmentVariables: params.deployParameters.environmentVariables, - templateFile: packageTemplatePath, - region: params.deployParameters.region, - stackName: params.deployParameters.destinationStackName, - s3Bucket: params.deployParameters.packageBucketName, - ecrRepo: params.deployParameters.ecrRepo, - }, - params.invoker - ) - } catch (err) { - // Handle sam deploy Errors to supplement the error message prior to writing it out - const error = err as Error - - getLogger().error(error) - - const errorMessage = enhanceAwsCloudFormationInstructions(String(err), params.deployParameters) - globals.outputChannel.appendLine(errorMessage) - - throw new Error('Deploy failed') - } -} - -async function deploy(params: { - deployParameters: DeploySamApplicationParameters - invoker: SamCliProcessInvoker - window: WindowFunctions -}): Promise { - globals.outputChannel.show(true) - getLogger().info(localize('AWS.samcli.deploy.workflow.start', 'Starting SAM Application deployment...')) - - const buildSuccessful = await buildOperation(params) - await packageOperation(params, buildSuccessful) - await deployOperation(params) - - getLogger().info( - localize( - 'AWS.samcli.deploy.workflow.success', - 'Deployed SAM Application to CloudFormation Stack: {0}', - params.deployParameters.destinationStackName - ) - ) - - void params.window.showInformationMessage( - localize('AWS.samcli.deploy.workflow.success.general', 'SAM Application deployment succeeded.') - ) -} - -function enhanceAwsCloudFormationInstructions( - message: string, - deployParameters: DeploySamApplicationParameters -): string { - // detect error message from https://github.com/aws/aws-cli/blob/4ff0cbacbac69a21d4dd701921fe0759cf7852ed/awscli/customizations/cloudformation/exceptions.py#L42 - // and append region to assist in troubleshooting the error - // (command uses CLI configured value--users that don't know this and omit region won't see error) - if ( - message.includes( - `aws cloudformation describe-stack-events --stack-name ${deployParameters.destinationStackName}` - ) - ) { - message += ` --region ${deployParameters.region}` - } - - return message -} - -function outputDeployError(error: Error) { - getLogger().error(error) - - globals.outputChannel.show(true) - getLogger().error('AWS.samcli.deploy.general.error', 'Error deploying a SAM Application. {0}', checklogs()) -} - -function getDefaultWindowFunctions(): WindowFunctions { - return { - setStatusBarMessage: vscode.window.setStatusBarMessage, - showErrorMessage: vscode.window.showErrorMessage, - showInformationMessage: vscode.window.showInformationMessage, - } -} diff --git a/packages/core/src/lambda/commands/listSamResources.ts b/packages/core/src/lambda/commands/listSamResources.ts new file mode 100644 index 00000000000..5a0d1678c9b --- /dev/null +++ b/packages/core/src/lambda/commands/listSamResources.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger' +import { runSamCliListResource } from '../../shared/sam/cli/samCliListResources' + +export interface StackResource { + LogicalResourceId: string + PhysicalResourceId: string +} + +/* +This function return exclusively the deployed resources +Newly added but yet-to-be deployed resources are not included in this result +*/ +export async function getDeployedResources(params: any) { + try { + const samCliListResourceOutput = await runSamCliListResource(params.listResourcesParams, params.invoker).then( + (output) => parseSamListResourceOutput(output) + ) + // Filter out resources that are not deployed + return samCliListResourceOutput.filter((resource) => resource.PhysicalResourceId !== '-') + } catch (err) { + const error = err as Error + getLogger().error(error) + } +} + +function parseSamListResourceOutput(output: any): StackResource[] { + try { + if ((Array.isArray(output) && output.length === 0) || '[]' === output) { + // Handle if the output is instance or stringify version of an empty array to avoid parsing error + return [] + } + return JSON.parse(output) as StackResource[] + } catch (error: any) { + void vscode.window.showErrorMessage(`Failed to parse SAM CLI output: ${error.message}`) + return [] + } +} diff --git a/packages/core/src/lambda/models/samLambdaRuntime.ts b/packages/core/src/lambda/models/samLambdaRuntime.ts index 754d910a24e..5b97ef06e2a 100644 --- a/packages/core/src/lambda/models/samLambdaRuntime.ts +++ b/packages/core/src/lambda/models/samLambdaRuntime.ts @@ -22,6 +22,7 @@ export enum RuntimeFamily { DotNet, Go, Java, + Ruby, } export type RuntimePackageType = 'Image' | 'Zip' @@ -57,8 +58,15 @@ export const pythonRuntimes: ImmutableSet = ImmutableSet([ 'python3.7', ]) export const goRuntimes: ImmutableSet = ImmutableSet(['go1.x']) -export const javaRuntimes: ImmutableSet = ImmutableSet(['java17', 'java11', 'java8', 'java8.al2']) -export const dotNetRuntimes: ImmutableSet = ImmutableSet(['dotnet6']) +export const javaRuntimes: ImmutableSet = ImmutableSet([ + 'java17', + 'java11', + 'java8', + 'java8.al2', + 'java21', +]) +export const dotNetRuntimes: ImmutableSet = ImmutableSet(['dotnet6', 'dotnet8']) +export const rubyRuntimes: ImmutableSet = ImmutableSet(['ruby3.2', 'ruby3.3']) /** * Deprecated runtimes can be found at https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html @@ -77,6 +85,8 @@ export const deprecatedRuntimes: ImmutableSet = ImmutableSet([ 'nodejs8.10', 'nodejs10.x', 'nodejs12.x', + 'ruby2.5', + 'ruby2.7', ]) const defaultRuntimes = ImmutableMap([ [RuntimeFamily.NodeJS, 'nodejs20.x'], @@ -84,6 +94,7 @@ const defaultRuntimes = ImmutableMap([ [RuntimeFamily.DotNet, 'dotnet6'], [RuntimeFamily.Go, 'go1.x'], [RuntimeFamily.Java, 'java17'], + [RuntimeFamily.Ruby, 'ruby3.3'], ]) export const samZipLambdaRuntimes: ImmutableSet = ImmutableSet.union([ @@ -157,6 +168,8 @@ export function getFamily(runtime: string): RuntimeFamily { return RuntimeFamily.Go } else if (javaRuntimes.has(runtime)) { return RuntimeFamily.Java + } else if (rubyRuntimes.has(runtime)) { + return RuntimeFamily.Ruby } return RuntimeFamily.Unknown } @@ -206,6 +219,10 @@ export function getRuntimeFamily(langId: string): RuntimeFamily { return RuntimeFamily.Python case 'go': return RuntimeFamily.Go + case 'java': + return RuntimeFamily.Java + case 'ruby': + return RuntimeFamily.Ruby default: return RuntimeFamily.Unknown } diff --git a/packages/core/src/lambda/vue/configEditor/samInvoke.css b/packages/core/src/lambda/vue/configEditor/samInvoke.css index d248e071a90..9ca2c8ef452 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvoke.css +++ b/packages/core/src/lambda/vue/configEditor/samInvoke.css @@ -1,7 +1,3 @@ -form { - padding: 15px; -} - .section-header { margin: 0px; margin-bottom: 10px; @@ -10,7 +6,9 @@ form { } textarea { - max-width: 100%; + color: var(--vscode-settings-textInputForeground); + background: var(--vscode-settings-textInputBackground); + border: 1px solid var(--vscode-settings-textInputBorder); } .config-item { @@ -47,7 +45,133 @@ textarea { margin-bottom: 16px; } +.header-buttons { + display: flex; + align-items: center; + margin-bottom: 20px; +} + #target-type-selector { margin-bottom: 15px; margin-left: 8px; } + +.form-row { + display: grid; + grid-template-columns: 150px 1fr; + margin-bottom: 10px; +} + +.form-control { + min-width: 170%; /* Set a minimum width */ + width: 100%; /* Allow the width to adjust based on content */ + display: inline-block; + flex-grow: 1; + margin-right: 0.5rem; +} + +.payload-options-button { + display: grid; + align-items: center; + border: none; + padding: 5px 10px; + cursor: pointer; + font-size: 0.9em; + margin-bottom: 10px; +} + +.payload-options-buttons { + display: flex; + align-items: center; + margin-top: 10px; + margin-bottom: 10px; +} + +.Icontainer { + margin-inline: auto; + margin-top: 5rem; +} + +.container { + width: 574px; + height: 824px; + top: 18px; + gap: 20px; + margin: auto; + left: 688px; + background-color: var(--vscode-editor-background); +} + +.container em { + display: block; + text-align: justify; +} + +.button-theme-primary { + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border: 1px solid var(--vscode-button-border); + padding: 8px 12px; +} +.button-theme-primary:hover:not(:disabled) { + background: var(--vscode-button-hoverBackground); + cursor: pointer; +} +.button-theme-secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + border: 1px solid var(--vscode-button-border); + padding: 8px 12px; +} +.button-theme-secondary:hover:not(:disabled) { + background: var(--vscode-button-secondaryHoverBackground); + cursor: pointer; +} + +.formfield { + display: flex; + align-items: center; + margin-bottom: 0.5rem; +} + +.payload-options-buttons { + display: flex; + align-items: center; + margin-top: 10px; + margin-bottom: 10px; +} + +.radio-selector { + width: 15px; + height: 15px; + border-radius: 50%; +} + +.label-selector { + padding-left: 7px; + font-weight: 500; + font-size: 13px; + line-height: 15.51px; + text-align: center; +} + +.form-row-select { + width: 387px; + height: 28px; + border: 1px; + border-radius: 5px; + gap: 4px; + padding: 2px 8px; +} + +.form-row-event-select { + width: 244px; + height: 28px; + margin-bottom: 15px; + margin-left: 8px; +} + +.runtime-description { + font-size: 12px; + margin-top: 5px; +} diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts index 7ae941d72e1..9e3eed9980b 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts +++ b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts @@ -36,9 +36,22 @@ import { VueWebview } from '../../../webviews/main' import { Commands } from '../../../shared/vscode/commands2' import { telemetry } from '../../../shared/telemetry/telemetry' import { fs } from '../../../shared' +import { ToolkitError } from '../../../shared' +import { ResourceNode } from '../../../awsService/appBuilder/explorer/nodes/resourceNode' const localize = nls.loadMessageBundle() +export interface ResourceData { + logicalId: string + region: string + arn: string + location: string + handler: string + runtime: string + stackName: string + source: string +} + export type AwsSamDebuggerConfigurationLoose = AwsSamDebuggerConfiguration & { invokeTarget: Omit< AwsSamDebuggerConfiguration['invokeTarget'], @@ -55,7 +68,7 @@ interface SampleQuickPickItem extends vscode.QuickPickItem { filename: string } -interface LaunchConfigPickItem extends vscode.QuickPickItem { +export interface LaunchConfigPickItem extends vscode.QuickPickItem { index: number config?: AwsSamDebuggerConfiguration } @@ -66,7 +79,8 @@ export class SamInvokeWebview extends VueWebview { public constructor( private readonly extContext: ExtContext, // TODO(sijaden): get rid of `ExtContext` - private readonly config?: AwsSamDebuggerConfiguration + private readonly config?: AwsSamDebuggerConfiguration, + private readonly data?: ResourceData ) { super(SamInvokeWebview.sourcePath) } @@ -79,11 +93,11 @@ export class SamInvokeWebview extends VueWebview { return this.config } - /** - * Open a quick pick containing the names of launch configs in the `launch.json` array. - * Filter out non-supported launch configs. - */ - public async loadSamLaunchConfig(): Promise { + public getResourceData() { + return this.data + } + + public async getSamLaunchConfigs(): Promise { // TODO: Find a better way to infer this. Might need another arg from the frontend (depends on the context in which the launch config is made?) const workspaceFolder = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] @@ -94,7 +108,17 @@ export class SamInvokeWebview extends VueWebview { } const uri = workspaceFolder.uri const launchConfig = new LaunchConfiguration(uri) - const pickerItems = await getLaunchConfigQuickPickItems(launchConfig, uri) + const pickerItems = await this.getLaunchConfigQuickPickItems(launchConfig, uri) + return pickerItems + } + + /** + * Open a quick pick containing the names of launch configs in the `launch.json` array. + * Filter out non-supported launch configs. + */ + public async loadSamLaunchConfig(): Promise { + const pickerItems: LaunchConfigPickItem[] = (await this.getSamLaunchConfigs()) || [] + if (pickerItems.length === 0) { pickerItems.push({ index: -1, @@ -151,9 +175,14 @@ export class SamInvokeWebview extends VueWebview { return sample } catch (err) { getLogger().error('Error getting manifest data..: %O', err as Error) + throw ToolkitError.chain(err, 'getting manifest data') } } + protected getTemplateRegistry() { + return globals.templateRegistry + } + /** * Get all templates in the registry. * Call back into the webview with the registry contents. @@ -161,7 +190,7 @@ export class SamInvokeWebview extends VueWebview { public async getTemplate() { const items: (vscode.QuickPickItem & { templatePath: string })[] = [] const noTemplate = 'NOTEMPLATEFOUND' - for (const template of (await globals.templateRegistry).items) { + for (const template of (await this.getTemplateRegistry()).items) { const resources = template.item.Resources if (resources) { for (const resource of Object.keys(resources)) { @@ -213,6 +242,41 @@ export class SamInvokeWebview extends VueWebview { } } + // This method serves as a wrapper around the backend function `openLaunchJsonFile`. + // The frontend cannot directly import and invoke backend functions like `openLaunchJsonFile` + // because doing so would break the webview environment by introducing server-side logic + // into client-side code. Instead, this method acts as an interface or bridge, allowing + // the frontend to request the backend to open the launch configuration file without + // directly coupling the frontend to backend-specific implementations. + public async openLaunchConfig() { + await openLaunchJsonFile() + } + + public async promptFile() { + const fileLocations = await vscode.window.showOpenDialog({ + openLabel: 'Open', + }) + + if (!fileLocations || fileLocations.length === 0) { + return undefined + } + + try { + const fileContent = await fs.readFileBytes(fileLocations[0].fsPath) + return { + sample: fileContent, + selectedFilePath: fileLocations[0].fsPath, + selectedFile: this.getFileName(fileLocations[0].fsPath), + } + } catch (e) { + getLogger().error('readFileSync: Failed to read file at path %O', fileLocations[0].fsPath, e) + throw ToolkitError.chain(e, 'Failed to read selected file') + } + } + + public getFileName(filePath: string): string { + return path.basename(filePath) + } /** * Open a quick pick containing the names of launch configs in the `launch.json` array, plus a "Create New Entry" entry. * On selecting a name, overwrite the existing entry in the `launch.json` array and resave the file. @@ -220,7 +284,7 @@ export class SamInvokeWebview extends VueWebview { * @param config Config to save */ public async saveLaunchConfig(config: AwsSamDebuggerConfiguration): Promise { - const uri = await getUriFromLaunchConfig(config) + const uri = await this.getUriFromLaunchConfig(config) if (!uri) { // TODO Localize void vscode.window.showErrorMessage( @@ -228,13 +292,14 @@ export class SamInvokeWebview extends VueWebview { ) return } + const launchConfig = new LaunchConfiguration(uri) - const launchConfigItems = await getLaunchConfigQuickPickItems(launchConfig, uri) + const launchConfigItems = await this.getLaunchConfigQuickPickItems(launchConfig, uri) const pickerItems = [ { label: addCodiconToString( 'add', - localize('AWS.command.addSamDebugConfiguration', 'Add Debug Configuration') + localize('AWS.command.addSamDebugConfiguration', 'Add Local Invoke and Debug Configuration') ), index: -1, alwaysShow: true, @@ -267,7 +332,7 @@ export class SamInvokeWebview extends VueWebview { const response = await input.promptUser({ inputBox: ib }) if (response) { await launchConfig.addDebugConfiguration(finalizeConfig(config, response)) - await openLaunchJsonFile() + await this.openLaunchConfig() } } else { // use existing label @@ -275,7 +340,7 @@ export class SamInvokeWebview extends VueWebview { finalizeConfig(config, pickerResponse.label), pickerResponse.index ) - await openLaunchJsonFile() + await this.openLaunchConfig() } } @@ -284,12 +349,12 @@ export class SamInvokeWebview extends VueWebview { * TODO: Post validation failures back to webview? * @param config Config to invoke */ - public async invokeLaunchConfig(config: AwsSamDebuggerConfiguration): Promise { + public async invokeLaunchConfig(config: AwsSamDebuggerConfiguration, source?: string): Promise { const finalConfig = finalizeConfig( resolveWorkspaceFolderVariable(undefined, config), 'Editor-Created Debug Config' ) - const targetUri = await getUriFromLaunchConfig(finalConfig) + const targetUri = await this.getUriFromLaunchConfig(finalConfig) const folder = targetUri ? vscode.workspace.getWorkspaceFolder(targetUri) : undefined // Cloud9 currently can't resolve the `aws-sam` debug config provider. @@ -298,12 +363,65 @@ export class SamInvokeWebview extends VueWebview { // (Cloud9 also doesn't currently have variable resolution support anyways) if (isCloud9()) { const provider = new SamDebugConfigProvider(this.extContext) - await provider.resolveDebugConfiguration(folder, finalConfig) + await provider.resolveDebugConfiguration(folder, finalConfig, undefined, source) } else { // startDebugging on VS Code goes through the whole resolution chain await vscode.debug.startDebugging(folder, finalConfig) } } + public async getLaunchConfigQuickPickItems( + launchConfig: LaunchConfiguration, + uri: vscode.Uri + ): Promise { + const existingConfigs = launchConfig.getDebugConfigurations() + const samValidator = new DefaultAwsSamDebugConfigurationValidator(vscode.workspace.getWorkspaceFolder(uri)) + const registry = await globals.templateRegistry + const mapped = existingConfigs.map((val, index) => { + return { + config: val as AwsSamDebuggerConfiguration, + index: index, + label: val.name, + } + }) + // XXX: can't use filter() with async predicate. + const filtered: LaunchConfigPickItem[] = [] + for (const c of mapped) { + const valid = await samValidator.validate(c.config, registry, true) + if (valid?.isValid) { + filtered.push(c) + } + } + return filtered + } + + public async getUriFromLaunchConfig(config: AwsSamDebuggerConfiguration): Promise { + let targetPath: string + if (isTemplateTargetProperties(config.invokeTarget)) { + targetPath = config.invokeTarget.templatePath + } else if (isCodeTargetProperties(config.invokeTarget)) { + targetPath = config.invokeTarget.projectRoot + } else { + // error + return undefined + } + if (path.isAbsolute(targetPath)) { + return vscode.Uri.file(targetPath) + } + // TODO: rework this logic (and config variables in general) + // we have too many places where we try to resolve these paths when it realistically can be + // in a single place. Much less bug-prone when it's centralized. + // the following line is a quick-fix for a very narrow edge-case + targetPath = targetPath.replace('${workspaceFolder}/', '') + const workspaceFolders = vscode.workspace.workspaceFolders || [] + for (const workspaceFolder of workspaceFolders) { + const absolutePath = tryGetAbsolutePath(workspaceFolder, targetPath) + if (await fs.exists(absolutePath)) { + return vscode.Uri.file(absolutePath) + } + } + + return undefined + } } const WebviewPanel = VueWebview.compilePanel(SamInvokeWebview) @@ -313,7 +431,7 @@ export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposa const webview = new WebviewPanel(context.extensionContext, context, launchConfig) await telemetry.sam_openConfigUi.run(async (span) => { await webview.show({ - title: localize('AWS.command.launchConfigForm.title', 'Edit SAM Debug Configuration'), + title: localize('AWS.command.launchConfigForm.title', 'Local Invoke and Debug Configuration'), // TODO: make this only open `Beside` when executed via CodeLens viewColumn: vscode.ViewColumn.Beside, }) @@ -321,58 +439,28 @@ export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposa }) } -async function getUriFromLaunchConfig(config: AwsSamDebuggerConfiguration): Promise { - let targetPath: string - if (isTemplateTargetProperties(config.invokeTarget)) { - targetPath = config.invokeTarget.templatePath - } else if (isCodeTargetProperties(config.invokeTarget)) { - targetPath = config.invokeTarget.projectRoot - } else { - // error - return undefined - } - if (path.isAbsolute(targetPath)) { - return vscode.Uri.file(targetPath) - } - // TODO: rework this logic (and config variables in general) - // we have too many places where we try to resolve these paths when it realistically can be - // in a single place. Much less bug-prone when it's centralized. - // the following line is a quick-fix for a very narrow edge-case - targetPath = targetPath.replace('${workspaceFolder}/', '') - const workspaceFolders = vscode.workspace.workspaceFolders || [] - for (const workspaceFolder of workspaceFolders) { - const absolutePath = tryGetAbsolutePath(workspaceFolder, targetPath) - if (await fs.exists(absolutePath)) { - return vscode.Uri.file(absolutePath) - } - } - - return undefined -} - -async function getLaunchConfigQuickPickItems( - launchConfig: LaunchConfiguration, - uri: vscode.Uri -): Promise { - const existingConfigs = launchConfig.getDebugConfigurations() - const samValidator = new DefaultAwsSamDebugConfigurationValidator(vscode.workspace.getWorkspaceFolder(uri)) - const registry = await globals.templateRegistry - const mapped = existingConfigs.map((val, index) => { - return { - config: val as AwsSamDebuggerConfiguration, - index: index, - label: val.name, - } +export async function registerSamDebugInvokeVueCommand(context: ExtContext, params: { resource: ResourceNode }) { + const launchConfig: AwsSamDebuggerConfiguration | undefined = undefined + const resource = params?.resource.resource + const source = 'AppBuilderLocalInvoke' + const webview = new WebviewPanel(context.extensionContext, context, launchConfig, { + logicalId: resource.resource.Id ?? '', + region: resource.region ?? '', + location: resource.location.fsPath, + handler: resource.resource.Handler!, + runtime: resource.resource.Runtime!, + arn: resource.functionArn ?? '', + stackName: resource.stackName ?? '', + source: source, + }) + await telemetry.sam_openConfigUi.run(async (span) => { + telemetry.record({ source: 'AppBuilderDebugger' }), + await webview.show({ + title: localize('AWS.command.launchConfigForm.title', 'Local Invoke and Debug Configuration'), + // TODO: make this only open `Beside` when executed via CodeLens + viewColumn: vscode.ViewColumn.Beside, + }) }) - // XXX: can't use filter() with async predicate. - const filtered: LaunchConfigPickItem[] = [] - for (const c of mapped) { - const valid = await samValidator.validate(c.config, registry, true) - if (valid?.isValid) { - filtered.push(c) - } - } - return filtered } export function finalizeConfig(config: AwsSamDebuggerConfiguration, name: string): AwsSamDebuggerConfiguration { diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue b/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue index 8d958ff8206..468d7393ac6 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue +++ b/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue @@ -4,277 +4,388 @@ diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeFrontend.ts b/packages/core/src/lambda/vue/configEditor/samInvokeFrontend.ts index 4bae01fd7dd..95f0fa9a14f 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvokeFrontend.ts +++ b/packages/core/src/lambda/vue/configEditor/samInvokeFrontend.ts @@ -8,7 +8,12 @@ import { defineComponent } from 'vue' import { AwsSamDebuggerConfiguration } from '../../../shared/sam/debugger/awsSamDebugConfiguration' -import { AwsSamDebuggerConfigurationLoose, SamInvokeWebview } from './samInvokeBackend' +import { + AwsSamDebuggerConfigurationLoose, + LaunchConfigPickItem, + ResourceData, + SamInvokeWebview, +} from './samInvokeBackend' import settingsPanel from '../../../webviews/components/settingsPanel.vue' import { WebviewClientFactory } from '../../../webviews/client' import saveData from '../../../webviews/mixins/saveData' @@ -34,6 +39,15 @@ interface SamInvokeVueData { parameters: VueDataLaunchPropertyObject containerBuild: boolean skipNewImageCheck: boolean + selectedConfig: LaunchConfigPickItem + payloadOption: string + selectedFile: string + selectedFilePath: string + selectedTestEvent: string + TestEvents: string[] + showNameInput: boolean + newTestEventName: string + resourceData: ResourceData | undefined } function newLaunchConfig(existingConfig?: AwsSamDebuggerConfiguration): AwsSamDebuggerConfigurationLoose { @@ -105,6 +119,11 @@ function initData() { parameters: { value: '', errorMsg: '' }, headers: { value: '', errorMsg: '' }, stageVariables: { value: '', errorMsg: '' }, + selectedConfig: { index: 0, config: undefined, label: 'new-config' }, + selectedTestEvent: '', + TestEvents: [], + showNameInput: false, + newTestEventName: '', } } @@ -114,30 +133,7 @@ export default defineComponent({ settingsPanel, }, created() { - client.init().then( - (config) => this.parseConfig(config), - (e) => { - console.error('client.init failed: %s', (e as Error).message) - } - ) - - client.getRuntimes().then( - (runtimes) => { - this.runtimes = runtimes - }, - (e) => { - console.error('client.getRuntimes failed: %s', (e as Error).message) - } - ) - - client.getCompanyName().then( - (o) => { - this.company = o - }, - (e) => { - console.error('client.getCompanyName failed: %s', (e as Error).message) - } - ) + this.setUpWebView() }, mixins: [saveData], data(): SamInvokeVueData { @@ -152,6 +148,10 @@ export default defineComponent({ ], runtimes: [], httpMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH'], + payloadOption: 'sampleEvents', + selectedFile: '', + selectedFilePath: '', + resourceData: undefined, } }, methods: { @@ -161,12 +161,18 @@ export default defineComponent({ this.headers.errorMsg = '' this.stageVariables.errorMsg = '' }, - launch() { + async launch() { const config = this.formatConfig() - config && - client.invokeLaunchConfig(config).catch((e) => { - console.error('invokeLaunchConfig failed: %s', (e as Error).message) - }) + + if (!config) { + return // Exit early if config is not available + } + + const source = this.resourceData?.source + + client.invokeLaunchConfig(config, source).catch((e: Error) => { + console.error(`invokeLaunchConfig failed: ${e.message}`) + }) }, save() { const config = this.formatConfig() @@ -183,16 +189,34 @@ export default defineComponent({ } ) }, - parseConfig(config?: AwsSamDebuggerConfiguration) { + async parseConfig(config?: AwsSamDebuggerConfiguration) { if (!config) { return } const company = this.company this.clearForm() this.launchConfig = newLaunchConfig(config) + if (config.lambda?.payload) { this.payload.value = JSON.stringify(config.lambda.payload.json, undefined, 4) } + + const localArgs = config.sam?.localArguments + + if (!localArgs && this.payload.value) { + this.payloadOption = 'sampleEvents' + this.selectedFile = '' + } + + if (localArgs?.includes('-e') || localArgs?.includes('--event')) { + const index = localArgs.findIndex((arg) => arg === '-e' || arg === '--event') + + if (index !== -1 && localArgs[index + 1]) { + this.payloadOption = 'localFile' + this.selectedFile = await client.getFileName(localArgs[index + 1]) + } + } + if (config.lambda?.environmentVariables) { this.environmentVariables.value = JSON.stringify(config.lambda?.environmentVariables) } @@ -259,6 +283,95 @@ export default defineComponent({ } } }, + async openLaunchJson() { + await client.openLaunchConfig() + }, + onFileChange(event: Event) { + const input = event.target as HTMLInputElement + if (input.files && input.files.length > 0) { + const file = input.files[0] + this.selectedFile = file.name + + // Use Blob.text() to read the file as text + file.text() + .then((text) => { + this.payload.value = text + }) + .catch((error) => { + console.error('Error reading file:', error) + }) + } + }, + async promptForFileLocation() { + const resp = await client.promptFile() + + if (resp) { + this.selectedFile = resp.selectedFile + this.launchConfig.sam = this.launchConfig.sam || {} + this.launchConfig.sam.localArguments = this.launchConfig.sam.localArguments || [] + + // Ensure only one '-e ' or '--event ' exists + const eventArgIndex = this.launchConfig.sam.localArguments.findIndex( + (arg) => arg === '-e' || arg === '--event' + ) + + if (eventArgIndex !== -1 && this.launchConfig.sam.localArguments[eventArgIndex + 1]) { + // Replace the existing file path for either '-e' or '--event' + this.launchConfig.sam.localArguments[eventArgIndex + 1] = resp.selectedFilePath + } else { + // Add '-e ' if not already present + this.launchConfig.sam.localArguments.push('-e', resp.selectedFilePath) + } + } + }, + showNameField() { + this.showNameInput = true + }, + setUpWebView() { + client.init().then( + (config) => this.parseConfig(config), + (e) => { + console.error('client.init failed: %s', (e as Error).message) + } + ) + + if (this.launchConfig.invokeTarget.templatePath === '') { + client.getResourceData().then( + (data) => { + this.resourceData = data + if (this.launchConfig && this.resourceData) { + this.launchConfig.invokeTarget.logicalId = this.resourceData.logicalId + this.launchConfig.invokeTarget.templatePath = this.resourceData.location + this.launchConfig.invokeTarget.lambdaHandler = this.resourceData.handler + if (this.launchConfig.lambda) { + this.launchConfig.lambda.runtime = this.resourceData.runtime + } + } + }, + (e) => { + console.error('client.getResourceData failed: %s', (e as Error).message) + } + ) + } + + client.getRuntimes().then( + (runtimes) => { + this.runtimes = runtimes + }, + (e) => { + console.error('client.getRuntimes failed: %s', (e as Error).message) + } + ) + + client.getCompanyName().then( + (o) => { + this.company = o + }, + (e) => { + console.error('client.getCompanyName failed: %s', (e as Error).message) + } + ) + }, formatConfig() { this.resetJsonErrors() @@ -279,6 +392,27 @@ export default defineComponent({ // propagate those through to the `postMessage` command, causing an error. We can stop // this by recursively accessing all primitive fields (which is what this line does) const launchConfig: AwsSamDebuggerConfigurationLoose = JSON.parse(JSON.stringify(this.launchConfig)) + const localArgs = launchConfig.sam?.localArguments + + const removeEventArg = () => { + if (localArgs) { + const eventArgIndex = localArgs?.findIndex((arg) => arg === '-e' || arg === '--event') + if (eventArgIndex !== -1) { + // Remove the event argument and its value + localArgs?.splice(eventArgIndex, 2) + } + } + } + + if (localArgs) { + if (this.payload && this.payloadOption !== 'localFile') { + removeEventArg() + } else if (this.payloadOption === 'localFile' && this.selectedFile) { + payloadJson = undefined + } else { + removeEventArg() + } + } return { ...launchConfig, @@ -314,7 +448,9 @@ export default defineComponent({ }, clearForm() { const init = initData() - Object.keys(init).forEach((k) => (this.$data[k as keyof typeof init] = init[k as keyof typeof init] as any)) + Object.keys(init).forEach((k) => { + ;(this as any)[k] = init[k as keyof typeof init] + }) }, }, }) diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index b75dd9477ba..36ea36a55c5 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -8,6 +8,7 @@ import { readFileSync } from 'fs' // eslint-disable-line no-restricted-imports import * as _ from 'lodash' import * as vscode from 'vscode' import { DefaultLambdaClient, LambdaClient } from '../../../shared/clients/lambdaClient' +import * as picker from '../../../shared/ui/picker' import { ExtContext } from '../../../shared/extensions' import { getLogger } from '../../../shared/logger' @@ -18,17 +19,35 @@ import { getSampleLambdaPayloads, SampleRequest } from '../../utils' import * as nls from 'vscode-nls' import { VueWebview } from '../../../webviews/main' -import { telemetry } from '../../../shared/telemetry/telemetry' -import { Result } from '../../../shared/telemetry/telemetry' +import { telemetry, Result } from '../../../shared/telemetry/telemetry' import { decodeBase64 } from '../../../shared' +import { + runSamCliRemoteTestEvents, + SamCliRemoteTestEventsParameters, + TestEventsOperation, +} from '../../../shared/sam/cli/samCliRemoteTestEvent' +import { getSamCliContext } from '../../../shared/sam/cli/samCliContext' +import { ToolkitError } from '../../../shared' +import { basename } from 'path' const localize = nls.loadMessageBundle() +type Event = { + name: string + region: string + arn: string + event?: string +} + export interface InitialData { FunctionName: string FunctionArn: string FunctionRegion: string InputSamples: SampleRequest[] + TestEvents?: string[] + Source?: string + StackName?: string + LogicalId?: string } export interface RemoteInvokeData { @@ -36,6 +55,15 @@ export interface RemoteInvokeData { selectedSampleRequest: string sampleText: string selectedFile: string + selectedFilePath: string + selectedTestEvent: string + payload: string + showNameInput: boolean + newTestEventName: string + selectedFunction: string +} +interface SampleQuickPickItem extends vscode.QuickPickItem { + filename: string } export class RemoteInvokeWebview extends VueWebview { @@ -54,7 +82,7 @@ export class RemoteInvokeWebview extends VueWebview { return this.data } - public async invokeLambda(input: string): Promise { + public async invokeLambda(input: string, source?: string): Promise { let result: Result = 'Succeeded' this.channel.show() @@ -79,17 +107,10 @@ export class RemoteInvokeWebview extends VueWebview { this.channel.appendLine('') result = 'Failed' } finally { - telemetry.lambda_invokeRemote.emit({ result, passive: false }) + telemetry.lambda_invokeRemote.emit({ result, passive: false, source: source }) } } - public async getSample(requestName: string) { - const sampleUrl = `${sampleRequestPath}${requestName}` - const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? '' - - return sample - } - public async promptFile() { const fileLocations = await vscode.window.showOpenDialog({ openLabel: 'Open', @@ -101,14 +122,109 @@ export class RemoteInvokeWebview extends VueWebview { try { const fileContent = readFileSync(fileLocations[0].fsPath, { encoding: 'utf8' }) - return { sample: fileContent, - selectedFile: fileLocations[0].path, + selectedFilePath: fileLocations[0].fsPath, + selectedFile: this.getFileName(fileLocations[0].fsPath), } } catch (e) { getLogger().error('readFileSync: Failed to read file at path %O', fileLocations[0].fsPath, e) - void vscode.window.showErrorMessage((e as Error).message) + throw ToolkitError.chain(e, 'Failed to read selected file') + } + } + + public async loadFile(fileLocations: string) { + return await this.readFile(fileLocations) + } + + private async readFile(filePath: string) { + if (!filePath) { + return undefined + } + const fileLocation = vscode.Uri.file(filePath) + try { + const fileContent = readFileSync(fileLocation.fsPath, { encoding: 'utf8' }) + + return { + sample: fileContent, + selectedFilePath: fileLocation.fsPath, + selectedFile: this.getFileName(fileLocation.fsPath), + } + } catch (e) { + getLogger().error('readFileSync: Failed to read file at path %O', fileLocation.fsPath, e) + throw ToolkitError.chain(e, 'Failed to read selected file') + } + } + + private getFileName(filePath: string): string { + return basename(filePath) + } + + public async listRemoteTestEvents(functionArn: string, region: string): Promise { + const params: SamCliRemoteTestEventsParameters = { + functionArn: functionArn, + operation: TestEventsOperation.List, + region: region, + } + const result = await this.remoteTestEvents(params) + return result.split('\n') + } + + public async createRemoteTestEvents(putEvent: Event) { + const params: SamCliRemoteTestEventsParameters = { + functionArn: putEvent.arn, + operation: TestEventsOperation.Put, + name: putEvent.name, + eventSample: putEvent.event, + region: putEvent.region, + } + return await this.remoteTestEvents(params) + } + public async getRemoteTestEvents(getEvents: Event) { + const params: SamCliRemoteTestEventsParameters = { + name: getEvents.name, + operation: TestEventsOperation.Get, + functionArn: getEvents.arn, + region: getEvents.region, + } + return await this.remoteTestEvents(params) + } + + private async remoteTestEvents(params: SamCliRemoteTestEventsParameters) { + return await runSamCliRemoteTestEvents(params, getSamCliContext().invoker) + } + + public async getSamplePayload(): Promise { + try { + const inputs: SampleQuickPickItem[] = (await getSampleLambdaPayloads()).map((entry) => { + return { label: entry.name ?? '', filename: entry.filename ?? '' } + }) + + const qp = picker.createQuickPick({ + items: inputs, + options: { + title: localize( + 'AWS.lambda.form.pickSampleInput', + 'Enter keywords to filter the list of sample events' + ), + }, + }) + + const choices = await picker.promptUser({ + picker: qp, + }) + const pickerResponse = picker.verifySinglePickerOutput(choices) + + if (!pickerResponse) { + return + } + const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}` + const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? '' + + return sample + } catch (err) { + getLogger().error('Error getting manifest data..: %O', err as Error) + throw ToolkitError.chain(err, 'getting manifest data') } } } @@ -126,17 +242,23 @@ export async function invokeRemoteLambda( */ outputChannel: vscode.OutputChannel functionNode: LambdaFunctionNode + source?: string } ) { const inputs = await getSampleLambdaPayloads() - const client = new DefaultLambdaClient(params.functionNode.regionCode) - + const resource: any = params.functionNode + const source: string = params.source || 'AwsExplorerRemoteInvoke' + const client = new DefaultLambdaClient(resource.regionCode) const wv = new Panel(context.extensionContext, context.outputChannel, client, { - FunctionName: params.functionNode.configuration.FunctionName ?? '', - FunctionArn: params.functionNode.configuration.FunctionArn ?? '', - FunctionRegion: params.functionNode.regionCode, + FunctionName: resource.configuration.FunctionName ?? '', + FunctionArn: resource.configuration.FunctionArn ?? '', + FunctionRegion: resource.regionCode, InputSamples: inputs, + TestEvents: [], + Source: source, }) - await wv.show({ title: localize('AWS.invokeLambda.title', 'Invoke Lambda {0}', params.functionNode.functionName) }) + await wv.show({ + title: localize('AWS.invokeLambda.title', 'Invoke Lambda {0}', resource.configuration.FunctionName), + }) } diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css new file mode 100644 index 00000000000..99f124e6b0c --- /dev/null +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css @@ -0,0 +1,132 @@ +.Icontainer { + margin-inline: auto; + margin-top: 5rem; +} + +h1 { + text-align: left; + margin-bottom: 20px; +} + +div { + width: 521px; +} + +.form-row { + display: grid; + grid-template-columns: 150px 1fr; + margin-bottom: 10px; +} +.form-row-select { + width: 387px; + height: 28px; + border: 1px; + border-radius: 5px; + gap: 4px; + padding: 2px 8px; +} + +.dynamic-span { + white-space: nowrap; + text-overflow: initial; + overflow: scroll; + width: 381px; + height: 28px; + font-weight: 500; + font-size: 13px; + line-height: 15.51px; +} + +.form-row-event-select { + width: 244px; + height: 28px; + margin-bottom: 15px; + margin-left: 8px; +} + +.payload-options { + display: grid; + grid-template-columns: 150px 1fr; + align-items: center; + margin-bottom: 10px; +} + +label { + margin-right: 10px; +} + +span, +select, +.payload-options { + display: block; +} + +textarea { + color: var(--vscode-settings-textInputForeground); + background: var(--vscode-settings-textInputBackground); + border: 1px solid var(--vscode-settings-textInputBorder); +} + +.payload-options-button { + display: grid; + align-items: center; + border: none; + padding: 5px 10px; + cursor: pointer; + font-size: 0.9em; + margin-bottom: 10px; +} + +.button-theme-primary { + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border: 1px solid var(--vscode-button-border); + padding: 8px 12px; +} +.button-theme-primary:hover:not(:disabled) { + background: var(--vscode-button-hoverBackground); + cursor: pointer; +} +.button-theme-secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + border: 1px solid var(--vscode-button-border); + padding: 8px 12px; +} +.button-theme-secondary:hover:not(:disabled) { + background: var(--vscode-button-secondaryHoverBackground); + cursor: pointer; +} + +.payload-options-buttons { + display: flex; + align-items: center; + margin-top: 10px; + margin-bottom: 10px; +} + +.radio-selector { + width: 15px; + height: 15px; + border-radius: 50%; +} + +.label-selector { + padding-left: 7px; + font-weight: 500; + font-size: 13px; + line-height: 15.51px; + text-align: center; +} + +.form-row-select { + display: grid; + grid-template-columns: 150px 1fr; + margin-bottom: 10px; +} + +.formfield { + display: flex; + align-items: center; + margin-bottom: 0.5rem; +} diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue index a6e0d70d0f3..9e06ef590ba 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue @@ -1,75 +1,128 @@ /*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -