diff --git a/package-lock.json b/package-lock.json index 6169de723..d33142a71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,8 @@ "@azure/arm-resources": "^4.2.2", "@azure/container-registry": "1.0.0-beta.5", "@azure/ms-rest-js": "^2.2.1", - "@microsoft/vscode-azext-azureutils": "^0.3.4", - "@microsoft/vscode-azext-utils": "^0.3.14", + "@microsoft/vscode-azext-azureutils": "^0.3.7", + "@microsoft/vscode-azext-utils": "^0.4.0", "dayjs": "^1.11.3", "dotenv": "^16.0.0", "open": "^8.0.4", @@ -25,7 +25,7 @@ }, "devDependencies": { "@microsoft/eslint-config-azuretools": "^0.1.0", - "@microsoft/vscode-azext-dev": "^0.1.2", + "@microsoft/vscode-azext-dev": "^0.1.5", "@types/fs-extra": "^8.1.1", "@types/git-url-parse": "^9.0.0", "@types/gulp": "^4.0.6", @@ -43,15 +43,15 @@ "mocha": "^10.1.0", "mocha-junit-reporter": "^2.0.0", "mocha-multi-reporters": "^1.1.7", - "ts-node": "^7.0.1", - "typescript": "^4.3.5", + "ts-node": "^10.9.1", + "typescript": "^4.9.4", "vsce": "^1.87.1", "vscode-test": "^1.5.2", "webpack": "^5.28.0", "webpack-cli": "^4.6.0" }, "engines": { - "vscode": "^1.66.0" + "vscode": "^1.74.0" } }, "node_modules/@azure/abort-controller": { @@ -590,6 +590,28 @@ "node": ">=4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", @@ -769,9 +791,9 @@ } }, "node_modules/@microsoft/vscode-azext-azureutils": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-0.3.4.tgz", - "integrity": "sha512-ABzi4b4UJcD0IuRGCjpJXyp5Z3yvS5Pqki9ZEvZxcowuqfHl6yCRe2oMZ1xrwkMDhuY8ox81eiarLCOSYkju6A==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-0.3.7.tgz", + "integrity": "sha512-GZVHRWD1V5ACVH3r6/4RvPc5fStoR3dBVn2A9azdvWtODVt+ZasApED8nOfEbMNfbXYdop9iWtSvF9+FMXmu1g==", "dependencies": { "@azure/arm-resources": "^5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", @@ -779,7 +801,7 @@ "@azure/arm-storage": "^17.0.0", "@azure/arm-storage-profile-2020-09-01-hybrid": "^2.0.0", "@azure/ms-rest-js": "^2.2.1", - "@microsoft/vscode-azext-utils": "^0.3.3", + "@microsoft/vscode-azext-utils": "^0.4.0", "semver": "^7.3.7", "vscode-nls": "^5.0.1" } @@ -807,14 +829,14 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@microsoft/vscode-azext-azureutils/node_modules/vscode-nls": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.0.1.tgz", - "integrity": "sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A==" + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==" }, "node_modules/@microsoft/vscode-azext-dev": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-dev/-/vscode-azext-dev-0.1.2.tgz", - "integrity": "sha512-6uZDiaNiW5TGcyO5RgmMWvzE3ozNuCW+eoKPKP7GCBMm2oM7AgVcaDOxeVUfeps1IAhvo9S0j/Mu7hbgSkQ7TA==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-dev/-/vscode-azext-dev-0.1.5.tgz", + "integrity": "sha512-HpDVKmqL/hRWVtMKpVfiW1ZKkhR4WmkauFA8Vo/vzUNpfSrWX543bAhCVLnCO22/5FjLudsxm3V8sgN0tKMuJA==", "dev": true, "dependencies": { "@azure/arm-subscriptions": "^2.0.0", @@ -825,7 +847,7 @@ "copy-webpack-plugin": "^6.0.0", "fs-extra": "^8.0.0", "terser-webpack-plugin": "^5.0.0", - "ts-loader": "^5.3.3", + "ts-loader": "^9.4.2", "webpack": "5.28.0" }, "bin": { @@ -996,34 +1018,12 @@ "node": ">=0.4.0" } }, - "node_modules/@microsoft/vscode-azext-dev/node_modules/enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@microsoft/vscode-azext-dev/node_modules/es-module-lexer": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==", "dev": true }, - "node_modules/@microsoft/vscode-azext-dev/node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/@microsoft/vscode-azext-dev/node_modules/webpack": { "version": "5.28.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.28.0.tgz", @@ -1084,9 +1084,9 @@ } }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.3.14.tgz", - "integrity": "sha512-ic9pzUA3hpP8NLnlMYn3ebrz33w27AuFxJmTY5t7Z7PY026/A/+T4SPTBaWsyMRnE2KLbr5iti6ByMhhCm0MMw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.4.0.tgz", + "integrity": "sha512-1mnYfVah6pULYXExR7nkDqFdU1oA8bddtWlPANcWatSjB5qZ4qKFWrnkR3DqGvVItPptiNzKHYRmNBwJ+NAG9g==", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", "dayjs": "^1.11.2", @@ -1212,6 +1212,30 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -1332,12 +1356,6 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, - "node_modules/@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true - }, "node_modules/@types/uglify-js": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", @@ -1425,6 +1443,12 @@ "node": ">= 8" } }, + "node_modules/@types/webpack/node_modules/@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", @@ -1847,6 +1871,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adal-node": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.3.tgz", @@ -2047,6 +2080,12 @@ "readable-stream": "^2.0.6" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2245,15 +2284,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -3303,6 +3333,12 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3798,17 +3834,16 @@ } }, "node_modules/enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10.13.0" } }, "node_modules/enquirer": { @@ -3843,18 +3878,6 @@ "node": ">=4" } }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7292,19 +7315,6 @@ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", "dev": true }, - "node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8836,12 +8846,6 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -10274,9 +10278,9 @@ } }, "node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, "engines": { "node": ">=6" @@ -10608,368 +10612,168 @@ } }, "node_modules/ts-loader": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.5.tgz", - "integrity": "sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", + "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", "dev": true, "dependencies": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^1.0.2", - "micromatch": "^3.1.4", - "semver": "^5.0.1" + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" }, "engines": { - "node": ">=6.11.5" + "node": ">=12.0.0" }, "peerDependencies": { - "typescript": "*" + "typescript": "*", + "webpack": "^5.0.0" } }, - "node_modules/ts-loader/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, "dependencies": { - "color-convert": "^1.9.0" + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" }, - "engines": { - "node": ">=4" + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, - "node_modules/ts-loader/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/ts-node/node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4.0" } }, - "node_modules/ts-loader/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.3.1" } }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/tsconfig-paths": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" } }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/ts-loader/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "engines": { - "node": ">=0.8.0" + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "node_modules/ts-loader/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "safe-buffer": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/ts-loader/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/ts-loader/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "engines": { - "node": ">=4" - } - }, - "node_modules/ts-loader/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ts-loader/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ts-loader/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ts-loader/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ts-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/ts-loader/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ts-loader/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ts-node": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", - "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", - "dev": true, - "dependencies": { - "arrify": "^1.0.0", - "buffer-from": "^1.1.0", - "diff": "^3.1.0", - "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.5.6", - "yn": "^2.0.0" - }, - "bin": { - "ts-node": "dist/bin.js" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/ts-node/node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", - "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typed-rest-client": { @@ -10990,9 +10794,9 @@ "dev": true }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -11262,6 +11066,12 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -11698,28 +11508,6 @@ "acorn": "^8" } }, - "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/webpack/node_modules/webpack-sources": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", @@ -11999,12 +11787,12 @@ } }, "node_modules/yn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/yocto-queue": { @@ -12515,6 +12303,27 @@ } } }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@discoveryjs/json-ext": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", @@ -12667,9 +12476,9 @@ } }, "@microsoft/vscode-azext-azureutils": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-0.3.4.tgz", - "integrity": "sha512-ABzi4b4UJcD0IuRGCjpJXyp5Z3yvS5Pqki9ZEvZxcowuqfHl6yCRe2oMZ1xrwkMDhuY8ox81eiarLCOSYkju6A==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-0.3.7.tgz", + "integrity": "sha512-GZVHRWD1V5ACVH3r6/4RvPc5fStoR3dBVn2A9azdvWtODVt+ZasApED8nOfEbMNfbXYdop9iWtSvF9+FMXmu1g==", "requires": { "@azure/arm-resources": "^5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", @@ -12677,7 +12486,7 @@ "@azure/arm-storage": "^17.0.0", "@azure/arm-storage-profile-2020-09-01-hybrid": "^2.0.0", "@azure/ms-rest-js": "^2.2.1", - "@microsoft/vscode-azext-utils": "^0.3.3", + "@microsoft/vscode-azext-utils": "^0.4.0", "semver": "^7.3.7", "vscode-nls": "^5.0.1" }, @@ -12702,16 +12511,16 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "vscode-nls": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.0.1.tgz", - "integrity": "sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A==" + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==" } } }, "@microsoft/vscode-azext-dev": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-dev/-/vscode-azext-dev-0.1.2.tgz", - "integrity": "sha512-6uZDiaNiW5TGcyO5RgmMWvzE3ozNuCW+eoKPKP7GCBMm2oM7AgVcaDOxeVUfeps1IAhvo9S0j/Mu7hbgSkQ7TA==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-dev/-/vscode-azext-dev-0.1.5.tgz", + "integrity": "sha512-HpDVKmqL/hRWVtMKpVfiW1ZKkhR4WmkauFA8Vo/vzUNpfSrWX543bAhCVLnCO22/5FjLudsxm3V8sgN0tKMuJA==", "dev": true, "requires": { "@azure/arm-subscriptions": "^2.0.0", @@ -12722,7 +12531,7 @@ "copy-webpack-plugin": "^6.0.0", "fs-extra": "^8.0.0", "terser-webpack-plugin": "^5.0.0", - "ts-loader": "^5.3.3", + "ts-loader": "^9.4.2", "webpack": "5.28.0" }, "dependencies": { @@ -12884,28 +12693,12 @@ "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true }, - "enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, "es-module-lexer": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==", "dev": true }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, "webpack": { "version": "5.28.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.28.0.tgz", @@ -12950,9 +12743,9 @@ } }, "@microsoft/vscode-azext-utils": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.3.14.tgz", - "integrity": "sha512-ic9pzUA3hpP8NLnlMYn3ebrz33w27AuFxJmTY5t7Z7PY026/A/+T4SPTBaWsyMRnE2KLbr5iti6ByMhhCm0MMw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.4.0.tgz", + "integrity": "sha512-1mnYfVah6pULYXExR7nkDqFdU1oA8bddtWlPANcWatSjB5qZ4qKFWrnkR3DqGvVItPptiNzKHYRmNBwJ+NAG9g==", "requires": { "@vscode/extension-telemetry": "^0.6.2", "dayjs": "^1.11.2", @@ -13049,6 +12842,30 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, "@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -13169,12 +12986,6 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, - "@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true - }, "@types/uglify-js": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", @@ -13240,6 +13051,14 @@ "@types/webpack-sources": "*", "anymatch": "^3.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "dev": true + } } }, "@types/webpack-sources": { @@ -13572,6 +13391,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, "adal-node": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.3.tgz", @@ -13731,6 +13556,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -13876,12 +13707,6 @@ "es-abstract": "^1.19.0" } }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -14695,6 +14520,12 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -15074,14 +14905,13 @@ } }, "enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" } }, "enquirer": { @@ -15104,15 +14934,6 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -17807,16 +17628,6 @@ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", "dev": true }, - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -18985,12 +18796,6 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -20104,9 +19909,9 @@ } }, "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, "tar": { @@ -20366,228 +20171,48 @@ "dev": true }, "ts-loader": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.5.tgz", - "integrity": "sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", + "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", "dev": true, "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^1.0.2", - "micromatch": "^3.1.4", - "semver": "^5.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" } }, "ts-node": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", - "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, "requires": { - "arrify": "^1.0.0", - "buffer-from": "^1.1.0", - "diff": "^3.1.0", + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.5.6", - "yn": "^2.0.0" + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" }, "dependencies": { + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true + }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true } } @@ -20671,9 +20296,9 @@ "dev": true }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, "uc.micro": { @@ -20900,6 +20525,12 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -21170,22 +20801,6 @@ "dev": true, "requires": {} }, - "enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, "webpack-sources": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", @@ -21482,9 +21097,9 @@ } }, "yn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true }, "yocto-queue": { diff --git a/package.json b/package.json index 53cb66872..bbddc5c7d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "icon": "resources/azure-containerapps.png", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "engines": { - "vscode": "^1.66.0" + "vscode": "^1.74.0" }, "repository": { "type": "git", @@ -27,31 +27,17 @@ "multi-root ready" ], "preview": true, - "activationEvents": [ - "onCommand:containerApps.viewProperties", - "onCommand:containerApps.openInPortal", - "onCommand:containerApps.reportIssue", - "onCommand:containerApps.createContainerApp", - "onCommand:containerApps.deployImage", - "onCommand:containerApps.deleteManagedEnvironment", - "onCommand:containerApps.deleteContainerApp", - "onCommand:containerApps.disableIngress", - "onCommand:containerApps.enableIngress", - "onCommand:containerApps.toggleVisibility", - "onCommand:containerApps.editTargetPort", - "onCommand:containerApps.chooseRevisionMode", - "onCommand:containerApps.activateRevision", - "onCommand:containerApps.deactivateRevision", - "onCommand:containerApps.restartRevision", - "onCommand:containerApps.createManagedEnvironment", - "onCommand:containerApps.browse", - "onCommand:containerApps.openConsoleInPortal", - "onCommand:containerApps.editScalingRange", - "onCommand:containerApps.addScaleRule" - ], + "activationEvents": [], "main": "./main.js", "contributes": { "x-azResources": { + "azure": { + "branches": [ + { + "type": "ContainerAppsEnvironment" + } + ] + }, "activation": { "onResolve": [ "microsoft.app/managedenvironments" @@ -67,16 +53,6 @@ ] }, "commands": [ - { - "command": "containerApps.viewProperties", - "title": "%containerApps.viewProperties%", - "category": "Azure Container Apps" - }, - { - "command": "containerApps.openInPortal", - "title": "%containerApps.openInPortal%", - "category": "Azure Container Apps" - }, { "command": "containerApps.reportIssue", "title": "%containerApps.reportIssue%", @@ -171,16 +147,6 @@ ], "menus": { "view/item/context": [ - { - "command": "containerApps.viewProperties", - "when": "view == azureResourceGroups && viewItem =~ /azResource/i && viewItem =~ /^(?!.*containerEnvironment).*/i", - "group": "z@1" - }, - { - "command": "containerApps.openInPortal", - "when": "view == azureResourceGroups && viewItem =~ /containerApp[^s]/i", - "group": "z@2" - }, { "command": "containerApps.createManagedEnvironment", "when": "view == azureResourceGroups && viewItem =~ /azureResourceTypeGroup/i && viewItem =~ /containerAppsEnvironment/i", @@ -266,16 +232,6 @@ "when": "view == azureResourceGroups && viewItem =~ /scaleRules/i", "group": "1@1" } - ], - "commandPalette": [ - { - "command": "containerApps.viewProperties", - "when": "never" - }, - { - "command": "containerApps.openInPortal", - "when": "never" - } ] }, "configuration": [ @@ -327,7 +283,7 @@ }, "devDependencies": { "@microsoft/eslint-config-azuretools": "^0.1.0", - "@microsoft/vscode-azext-dev": "^0.1.2", + "@microsoft/vscode-azext-dev": "^0.1.5", "@types/fs-extra": "^8.1.1", "@types/git-url-parse": "^9.0.0", "@types/gulp": "^4.0.6", @@ -345,8 +301,8 @@ "mocha": "^10.1.0", "mocha-junit-reporter": "^2.0.0", "mocha-multi-reporters": "^1.1.7", - "ts-node": "^7.0.1", - "typescript": "^4.3.5", + "ts-node": "^10.9.1", + "typescript": "^4.9.4", "vsce": "^1.87.1", "vscode-test": "^1.5.2", "webpack": "^5.28.0", @@ -359,8 +315,8 @@ "@azure/arm-resources": "^4.2.2", "@azure/container-registry": "1.0.0-beta.5", "@azure/ms-rest-js": "^2.2.1", - "@microsoft/vscode-azext-azureutils": "^0.3.4", - "@microsoft/vscode-azext-utils": "^0.3.14", + "@microsoft/vscode-azext-azureutils": "^0.3.7", + "@microsoft/vscode-azext-utils": "^0.4.0", "dayjs": "^1.11.3", "dotenv": "^16.0.0", "open": "^8.0.4", diff --git a/resources/managedEnvironment.svg b/resources/managedEnvironment.svg new file mode 100644 index 000000000..d6186a366 --- /dev/null +++ b/resources/managedEnvironment.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ContainerAppsResolver.ts b/src/ContainerAppsResolver.ts deleted file mode 100644 index b0e7f1922..000000000 --- a/src/ContainerAppsResolver.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -import { ContainerAppsAPIClient, ManagedEnvironment } from "@azure/arm-appcontainers"; -import { getResourceGroupFromId } from "@microsoft/vscode-azext-azureutils"; -import { callWithTelemetryAndErrorHandling, IActionContext, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; -import { AppResource, AppResourceResolver, ResolvedAppResourceBase } from "@microsoft/vscode-azext-utils/hostapi"; -import { managedEnvironmentsAppProvider } from "./constants"; -import { ResolvedContainerEnvironmentResource } from "./tree/ResolvedContainerAppsResource"; -import { createContainerAppsAPIClient } from "./utils/azureClients"; - -export class ContainerAppsResolver implements AppResourceResolver { - public async resolveResource(subContext: ISubscriptionContext, resource: AppResource): Promise { - return await callWithTelemetryAndErrorHandling('resolveResource', async (context: IActionContext) => { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, subContext]); - const rgName: string = getResourceGroupFromId(resource.id); - const me: ManagedEnvironment = await client.managedEnvironments.get(rgName, resource.name); - return new ResolvedContainerEnvironmentResource(subContext, me); - }) ?? null; - } - - public matchesResource(resource: AppResource): boolean { - return resource.type.toLowerCase() === managedEnvironmentsAppProvider.toLowerCase(); - } -} diff --git a/src/commands/api/revealTreeItem.ts b/src/commands/api/revealTreeItem.ts deleted file mode 100644 index d13589351..000000000 --- a/src/commands/api/revealTreeItem.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem, callWithTelemetryAndErrorHandling, IActionContext } from "@microsoft/vscode-azext-utils"; -import { ext } from "../../extensionVariables"; - -export async function revealTreeItem(resourceId: string): Promise { - return await callWithTelemetryAndErrorHandling('api.revealTreeItem', async (context: IActionContext) => { - const node: AzExtTreeItem | undefined = await ext.rgApi.appResourceTree.findTreeItem(resourceId, { ...context, loadAll: true }); - if (node) { - await ext.rgApi.appResourceTreeView.reveal(node, { select: true, focus: true, expand: true }); - } - }); -} diff --git a/src/commands/browse.ts b/src/commands/browse.ts deleted file mode 100644 index 7344d4651..000000000 --- a/src/commands/browse.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IActionContext } from '@microsoft/vscode-azext-utils'; -import { rootFilter } from '../constants'; -import { ext } from '../extensionVariables'; -import { ContainerAppTreeItem } from '../tree/ContainerAppTreeItem'; - -export async function browse(context: IActionContext, node?: ContainerAppTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: ContainerAppTreeItem.contextValueRegExp - }); - } - - await node.browse(); -} diff --git a/src/commands/browseContainerApp.ts b/src/commands/browseContainerApp.ts new file mode 100644 index 000000000..3c6e518d1 --- /dev/null +++ b/src/commands/browseContainerApp.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ContainerApp } from '@azure/arm-appcontainers'; +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { ContainerAppItem, isIngressEnabled } from '../tree/ContainerAppItem'; +import { localize } from '../utils/localize'; +import { openUrl } from '../utils/openUrl'; +import { pickContainerApp } from '../utils/pickContainerApp'; + +export async function browseContainerAppNode(context: IActionContext, node?: ContainerAppItem): Promise { + node ??= await pickContainerApp(context) + await browseContainerApp(node.containerApp); +} + +export async function browseContainerApp(containerApp: ContainerApp): Promise { + if (isIngressEnabled(containerApp)) { + return await openUrl(`https://${containerApp.configuration.ingress.fqdn}`); + } + + throw new Error(localize('enableIngress', 'Enable ingress to perform this action.')); +} diff --git a/src/commands/chooseRevisionMode.ts b/src/commands/chooseRevisionMode.ts index 58540bdf7..f79ce32fb 100644 --- a/src/commands/chooseRevisionMode.ts +++ b/src/commands/chooseRevisionMode.ts @@ -3,51 +3,61 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { KnownActiveRevisionsMode } from "@azure/arm-appcontainers"; import { IActionContext, IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; import { ProgressLocation, window } from "vscode"; -import { RevisionConstants, rootFilter } from "../constants"; import { ext } from "../extensionVariables"; -import { ContainerAppTreeItem } from "../tree/ContainerAppTreeItem"; -import { RevisionsTreeItem } from "../tree/RevisionsTreeItem"; +import { ContainerAppModel, refreshContainerApp } from "../tree/ContainerAppItem"; +import { ContainerAppsItem } from "../tree/ContainerAppsBranchDataProvider"; import { localize } from "../utils/localize"; -import { nonNullValue } from "../utils/nonNull"; +import { pickContainerApp } from "../utils/pickContainerApp"; import { updateContainerApp } from "./updateContainerApp"; -export async function chooseRevisionMode(context: IActionContext, node?: ContainerAppTreeItem | RevisionsTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: ContainerAppTreeItem.contextValueRegExp - }); - } +export async function chooseRevisionMode(context: IActionContext, node?: ContainerAppsItem): Promise { + const { subscription, containerApp } = node ?? await pickContainerApp(context); - if (node instanceof RevisionsTreeItem) { - node = node.parent; - } + const pickedRevisionMode = await pickRevisionsMode(context, containerApp); + // only update it if it's actually different + if (containerApp.revisionsMode !== pickedRevisionMode) { + const updating = localize('updatingRevision', 'Updating revision mode of "{0}" to "{1}"...', containerApp.name, pickedRevisionMode); + ext.outputChannel.appendLog(updating); - const picks: IAzureQuickPickItem[] = [RevisionConstants.single, RevisionConstants.multiple]; - const placeHolder = localize('chooseRevision', 'Choose revision mode'); + await window.withProgress({ location: ProgressLocation.Notification, title: updating }, async (): Promise => { + await updateContainerApp(context, subscription, containerApp, { configuration: { activeRevisionsMode: pickedRevisionMode } }); + refreshContainerApp(containerApp.id); + }); - for (const pick of picks) { - pick.description = pick.data === node.getRevisionMode() ? localize('current', ' current') : undefined; + const updated = localize('updatedRevision', 'Updated revision mode of "{0}" to "{1}".', containerApp.name, pickedRevisionMode); + void window.showInformationMessage(updated); + ext.outputChannel.appendLog(updated); } +} - const result = await context.ui.showQuickPick(picks, { placeHolder, suppressPersistence: true }); - - // only update it if it's actually different - if (node.getRevisionMode() !== result.data) { - const updating = localize('updatingRevision', 'Updating revision mode of "{0}" to "{1}"...', node.name, result.data); - const updated = localize('updatedRevision', 'Updated revision mode of "{0}" to "{1}".', node.name, result.data); +function getRevisionsModePicks(containerApp: ContainerAppModel): IAzureQuickPickItem[] { - await window.withProgress({ location: ProgressLocation.Notification, title: updating }, async (): Promise => { - const pNode = nonNullValue(node) as ContainerAppTreeItem; - ext.outputChannel.appendLog(updating); + function appendCurrent(description: string, revisionsMode: KnownActiveRevisionsMode): string { + return revisionsMode === containerApp.revisionsMode ? `${description} (current)` : description; + } - await updateContainerApp(context, pNode, { configuration: { activeRevisionsMode: result.data } }); - await node?.parent?.refresh(context); + return [ + { + label: localize('multiple', 'Multiple'), + description: appendCurrent(localize('multipleDesc', 'Several revisions active simultaneously'), KnownActiveRevisionsMode.Multiple), + data: KnownActiveRevisionsMode.Multiple, + }, + { + label: localize('single', 'Single'), + description: appendCurrent(localize('singleDesc', 'One active revision at a time'), KnownActiveRevisionsMode.Single), + data: KnownActiveRevisionsMode.Single, + }, + ]; +} - void window.showInformationMessage(updated); - ext.outputChannel.appendLog(updated); - }); - } +async function pickRevisionsMode(context: IActionContext, containerApp: ContainerAppModel): Promise { + const placeHolder = localize('chooseRevision', 'Choose revision mode'); + const result = await context.ui.showQuickPick(getRevisionsModePicks(containerApp), { + placeHolder, + suppressPersistence: true, + }); + return result.data; } diff --git a/src/commands/createContainerApp/ContainerAppCreateStep.ts b/src/commands/createContainerApp/ContainerAppCreateStep.ts index 1f22532b6..941b93696 100644 --- a/src/commands/createContainerApp/ContainerAppCreateStep.ts +++ b/src/commands/createContainerApp/ContainerAppCreateStep.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContainerAppsAPIClient, Ingress, RegistryCredentials, Secret } from "@azure/arm-appcontainers"; +import { ContainerAppsAPIClient, Ingress, KnownActiveRevisionsMode, RegistryCredentials, Secret } from "@azure/arm-appcontainers"; import { LocationListStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; import { Progress } from "vscode"; -import { containerAppsWebProvider, RevisionConstants } from "../../constants"; +import { containerAppsWebProvider } from "../../constants"; import { ext } from "../../extensionVariables"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; import { createContainerAppsAPIClient } from "../../utils/azureClients"; import { localize } from "../../utils/localize"; import { nonNullProp } from "../../utils/nonNull"; @@ -67,14 +68,14 @@ export class ContainerAppCreateStep extends AzureWizardExecuteStep & Partial, node?: ManagedEnvironmentTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { filter: rootFilter }); - } +export async function createContainerApp(context: IActionContext & Partial & Partial, node?: ManagedEnvironmentItem): Promise { + node ??= await pickEnvironment(context); - const caNode: ContainerAppTreeItem = await node.createChild(context); - void showContainerAppCreated(caNode); + const wizardContext: IContainerAppWithActivityContext = { + ...context, + ...createSubscriptionContext(node.subscription), + managedEnvironmentId: node.managedEnvironment.id, + ...(await createActivityContext()) + }; - await node.refresh(context); - return caNode; + const title: string = localize('createContainerApp', 'Create Container App'); + + const promptSteps: AzureWizardPromptStep[] = [ + new ContainerAppNameStep(), + new ContainerRegistryListStep(), + new EnvironmentVariablesListStep(), + new EnableIngressStep(), + ]; + + const executeSteps: AzureWizardExecuteStep[] = [ + new VerifyProvidersStep([webProvider]), + new ContainerAppCreateStep(), + ]; + + wizardContext.newResourceGroupName = node.resource.resourceGroup; + await LocationListStep.setLocation(wizardContext, nonNullProp(node.resource, 'location')); + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title, + promptSteps, + executeSteps, + showLoadingPrompt: true + }); + + await wizard.prompt(); + const newContainerAppName = nonNullProp(wizardContext, 'newContainerAppName'); + + await ext.state.showCreatingChild( + node.managedEnvironment.id, + localize('creatingContainerApp', 'Creating Container App "{0}"...', newContainerAppName), + async () => { + wizardContext.activityTitle = localize('createNamedContainerApp', 'Create Container App "{0}"', newContainerAppName); + try { + await wizard.execute(); + } finally { + // refresh this node even if create fails because container app provision failure throws an error, but still creates a container app + // ext.state.notifyChildrenChanged(node.managedEnvironment.id); + } + }); + + const createdContainerApp = nonNullProp(wizardContext, 'containerApp'); + void showContainerAppCreated(createdContainerApp); + return new ContainerAppItem(node.subscription, createdContainerApp); } diff --git a/src/commands/createContainerApp/showContainerAppCreated.ts b/src/commands/createContainerApp/showContainerAppCreated.ts index a31817b96..667bd4196 100644 --- a/src/commands/createContainerApp/showContainerAppCreated.ts +++ b/src/commands/createContainerApp/showContainerAppCreated.ts @@ -3,28 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ContainerApp } from "@azure/arm-appcontainers"; import { callWithTelemetryAndErrorHandling, IActionContext } from "@microsoft/vscode-azext-utils"; import { MessageItem, window } from "vscode"; import { ext } from "../../extensionVariables"; -import { ContainerAppTreeItem } from "../../tree/ContainerAppTreeItem"; +import { isIngressEnabled } from "../../tree/ContainerAppItem"; import { localize } from "../../utils/localize"; +import { browseContainerApp } from "../browseContainerApp"; -export async function showContainerAppCreated(node: ContainerAppTreeItem, isUpdate: boolean = false): Promise { +export async function showContainerAppCreated(containerApp: ContainerApp, isUpdate: boolean = false): Promise { return await callWithTelemetryAndErrorHandling('containerApps.showCaCreated', async (context: IActionContext) => { - const createdCa: string = localize('createdCa', 'Successfully created new container app "{0}".', node.name); - const createdRevision = localize('createdRevision', 'Created a new revision "{1}" for container app "{0}"', node.name, node.data.latestRevisionName); + const createdCa: string = localize('createdCa', 'Successfully created new container app "{0}".', containerApp.name); + const createdRevision = localize('createdRevision', 'Created a new revision "{1}" for container app "{0}"', containerApp.name, containerApp.latestRevisionName); const message = isUpdate ? createdRevision : createdCa; ext.outputChannel.appendLog(message); const browse: MessageItem = { title: localize('browse', 'Browse') }; const buttons: MessageItem[] = []; - if (node.ingressEnabled()) { buttons.push(browse) } - await window.showInformationMessage(message, ...buttons).then(async (result) => { - context.telemetry.properties.clicked = 'canceled'; - if (result === browse) { - await node.browse(); - context.telemetry.properties.clicked = 'browse'; - } - }); + if (isIngressEnabled(containerApp)) { + buttons.push(browse) + } + const result = await window.showInformationMessage(message, ...buttons) + + context.telemetry.properties.clicked = 'canceled'; + if (result === browse) { + await browseContainerApp(containerApp); + context.telemetry.properties.clicked = 'browse'; + } }); } diff --git a/src/commands/createManagedEnvironment/IManagedEnvironmentContext.ts b/src/commands/createManagedEnvironment/IManagedEnvironmentContext.ts index 12ca9c6f3..be14cea09 100644 --- a/src/commands/createManagedEnvironment/IManagedEnvironmentContext.ts +++ b/src/commands/createManagedEnvironment/IManagedEnvironmentContext.ts @@ -6,9 +6,9 @@ import { ManagedEnvironment } from "@azure/arm-appcontainers"; import { Workspace } from '@azure/arm-operationalinsights'; import { IResourceGroupWizardContext } from '@microsoft/vscode-azext-azureutils'; -import { ExecuteActivityContext, ICreateChildImplContext } from '@microsoft/vscode-azext-utils'; +import { ExecuteActivityContext } from '@microsoft/vscode-azext-utils'; -export interface IManagedEnvironmentContext extends IResourceGroupWizardContext, ICreateChildImplContext, ExecuteActivityContext { +export interface IManagedEnvironmentContext extends IResourceGroupWizardContext, ExecuteActivityContext { newManagedEnvironmentName?: string; logAnalyticsWorkspace?: Workspace; diff --git a/src/commands/createManagedEnvironment/createManagedEnvironment.ts b/src/commands/createManagedEnvironment/createManagedEnvironment.ts index 7c7cbc9b5..1c6ca5d10 100644 --- a/src/commands/createManagedEnvironment/createManagedEnvironment.ts +++ b/src/commands/createManagedEnvironment/createManagedEnvironment.ts @@ -3,18 +3,48 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SubscriptionTreeItemBase } from "@microsoft/vscode-azext-azureutils"; -import { AzExtParentTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; +import { LocationListStep, ResourceGroupCreateStep, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, nonNullProp, subscriptionExperience } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; import { ext } from "../../extensionVariables"; -import { ManagedEnvironmentTreeItem } from "../../tree/ManagedEnvironmentTreeItem"; -import { SubscriptionTreeItem } from "../../tree/SubscriptionTreeItem"; +import { createSubscriptionContext } from "../../tree/ContainerAppsBranchDataProvider"; +import { createActivityContext } from "../../utils/activityUtils"; +import { localize } from "../../utils/localize"; import { IManagedEnvironmentContext } from "./IManagedEnvironmentContext"; +import { LogAnalyticsCreateStep } from "./LogAnalyticsCreateStep"; +import { ManagedEnvironmentCreateStep } from "./ManagedEnvironmentCreateStep"; +import { ManagedEnvironmentNameStep } from "./ManagedEnvironmentNameStep"; -export async function createManagedEnvironment(context: IActionContext, node?: AzExtParentTreeItem): Promise { - if (!node) { - node = await ext.rgApi.appResourceTree.showTreeItemPicker(SubscriptionTreeItemBase.contextValue, context); - } - const managedEnvironment = await SubscriptionTreeItem.createChild(context as IManagedEnvironmentContext, node as SubscriptionTreeItemBase); - await ext.rgApi.appResourceTree.refresh(context); - return managedEnvironment; +export async function createManagedEnvironment(context: IActionContext, node?: { subscription: AzureSubscription }): Promise { + const subscription = node?.subscription ?? await subscriptionExperience(context, ext.rgApiV2.resources.azureResourceTreeDataProvider); + + const wizardContext: IManagedEnvironmentContext = { + ...context, + ...createSubscriptionContext(subscription), + ...(await createActivityContext()) + }; + + const title: string = localize('createManagedEnv', 'Create Container Apps environment'); + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + promptSteps.push(new ManagedEnvironmentNameStep()); + executeSteps.push(new VerifyProvidersStep(['Microsoft.App', 'Microsoft.OperationalInsights']), new ResourceGroupCreateStep(), new LogAnalyticsCreateStep(), new ManagedEnvironmentCreateStep()); + LocationListStep.addProviderForFiltering(wizardContext, 'Microsoft.App', 'managedEnvironments'); + LocationListStep.addStep(wizardContext, promptSteps); + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title, + promptSteps, + executeSteps, + showLoadingPrompt: true + }); + + await wizard.prompt(); + const newManagedEnvName = nonNullProp(wizardContext, 'newManagedEnvironmentName'); + wizardContext.newResourceGroupName = newManagedEnvName; + wizardContext.activityTitle = localize('createNamedManagedEnv', 'Create Container Apps environment "{0}"', newManagedEnvName); + await wizard.execute(); + + ext.branchDataProvider.refresh(); } diff --git a/src/commands/deleteContainerApp/deleteContainerApp.ts b/src/commands/deleteContainerApp/deleteContainerApp.ts new file mode 100644 index 000000000..7d4113164 --- /dev/null +++ b/src/commands/deleteContainerApp/deleteContainerApp.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { deleteNode } from "../deleteNode"; + +export async function deleteContainerApp(context: IActionContext, node?: ContainerAppItem): Promise { + return deleteNode(context, ContainerAppItem.contextValueRegExp, node); +} diff --git a/src/commands/deleteManagedEnvironment/DeleteEnvironmentConfirmationStep.ts b/src/commands/deleteManagedEnvironment/DeleteEnvironmentConfirmationStep.ts index f8f968d4b..1c13fdbf9 100644 --- a/src/commands/deleteManagedEnvironment/DeleteEnvironmentConfirmationStep.ts +++ b/src/commands/deleteManagedEnvironment/DeleteEnvironmentConfirmationStep.ts @@ -22,7 +22,7 @@ export class DeleteEnvironmentConfirmationStep extends AzureWizardPromptStep { + const { subscription, managedEnvironment } = node ?? await pickEnvironment(context); + + const containerApps = await ContainerAppItem.List(context, subscription, managedEnvironment.id); + const resourceGroupName = getResourceGroupFromId(managedEnvironment.id); + const deleteManagedEnvironment: string = localize('deleteManagedEnvironment', 'Delete Container Apps Environment "{0}"', managedEnvironment.name); + + const wizardContext: IDeleteManagedEnvironmentWizardContext = { + activityTitle: deleteManagedEnvironment, + containerAppNames: containerApps.map(ca => ca.name), + managedEnvironmentName: managedEnvironment.name, + resourceGroupName: resourceGroupName, + subscription: createSubscriptionContext(subscription), + ...context, + ...(await createActivityContext()) + }; + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + promptSteps: [new DeleteEnvironmentConfirmationStep()], + executeSteps: [new DeleteAllContainerAppsStep(), new DeleteManagedEnvironmentStep()] + }); + + if (!context.suppressPrompt) { + await wizard.prompt(); + } + + await ext.state.runWithTemporaryDescription(managedEnvironment.id, localize('deleting', 'Deleting...'), async () => { + await wizard.execute(); + ext.branchDataProvider.refresh(); + }); +} diff --git a/src/commands/deleteNode.ts b/src/commands/deleteNode.ts index c5f526c1b..02f428d99 100644 --- a/src/commands/deleteNode.ts +++ b/src/commands/deleteNode.ts @@ -3,17 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzExtTreeItem, IActionContext } from '@microsoft/vscode-azext-utils'; -import { rootFilter } from '../constants'; +import { AzExtResourceType, azureResourceExperience, IActionContext } from '@microsoft/vscode-azext-utils'; import { ext } from '../extensionVariables'; -export async function deleteNode(context: IActionContext, expectedContextValue: string | RegExp, node?: AzExtTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource({ ...context, suppressCreatePick: true }, { - filter: rootFilter, - expectedChildContextValue: expectedContextValue - }); +export async function deleteNode(context: IActionContext, expectedContextValue: string | RegExp, node?: IDeletable): Promise { + node ??= await azureResourceExperience(context, ext.rgApiV2.resources.azureResourceTreeDataProvider, AzExtResourceType.ContainerAppsEnvironment, { + include: [expectedContextValue] + }); + + if (!canDelete(node)) { + throw new Error('Node does not support delete'); } - await node.deleteTreeItem(context); + await node.delete(context); +} + +export interface IDeletable { + delete(context: IActionContext): Promise; +} + +function canDelete(node: unknown): node is IDeletable { + return !!(node as IDeletable).delete; } diff --git a/src/commands/deployImage/deployImage.ts b/src/commands/deployImage/deployImage.ts index e2b3bd3e5..4aef99e1a 100644 --- a/src/commands/deployImage/deployImage.ts +++ b/src/commands/deployImage/deployImage.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContainerAppsAPIClient } from "@azure/arm-appcontainers"; +import { ContainerApp, KnownActiveRevisionsMode } from "@azure/arm-appcontainers"; import { VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, ITreeItemPickerContext } from "@microsoft/vscode-azext-utils"; import { MessageItem, ProgressLocation, window } from "vscode"; -import { RevisionConstants, rootFilter, webProvider } from "../../constants"; +import { webProvider } from "../../constants"; import { ext } from "../../extensionVariables"; -import { ContainerAppTreeItem } from "../../tree/ContainerAppTreeItem"; -import { createContainerAppsAPIClient } from "../../utils/azureClients"; +import { ContainerAppItem, getContainerEnvelopeWithSecrets, refreshContainerApp } from "../../tree/ContainerAppItem"; +import { createSubscriptionContext } from "../../tree/ContainerAppsBranchDataProvider"; import { localize } from "../../utils/localize"; -import { nonNullValue } from "../../utils/nonNull"; +import { pickContainerApp } from "../../utils/pickContainerApp"; import { EnvironmentVariablesListStep } from "../createContainerApp/EnvironmentVariablesListStep"; import { getLoginServer } from "../createContainerApp/getLoginServer"; import { showContainerAppCreated } from "../createContainerApp/showContainerAppCreated"; @@ -21,27 +21,29 @@ import { ContainerRegistryListStep } from "./ContainerRegistryListStep"; import { getContainerNameForImage } from "./getContainerNameForImage"; import { IDeployImageContext } from "./IDeployImageContext"; -export async function deployImage(context: ITreeItemPickerContext & Partial, node?: ContainerAppTreeItem): Promise { +export async function deployImage(context: ITreeItemPickerContext & Partial, node?: ContainerAppItem): Promise { if (!node) { context.suppressCreatePick = true; - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: ContainerAppTreeItem.contextValueRegExp - }); + node = await pickContainerApp(context); } + const { subscription, containerApp } = node; - const wizardContext: IDeployImageContext = { ...context, ...node.subscription, targetContainer: node.data }; + const wizardContext: IDeployImageContext = { + ...context, + ...createSubscriptionContext(subscription), + targetContainer: containerApp + }; - if (hasUnsupportedFeatures(node)) { - const warning: string = node.getRevisionMode() === RevisionConstants.single.data ? - localize('confirmDeploySingle', 'Are you sure you want to deploy to "{0}"? This will overwrite the active revision and unsupported features in VS Code will be lost.', node.name) : - localize('confirmDeployMultiple', 'Are you sure you want to deploy to "{0}"? Unsupported features in VS Code will be lost in the new revision.', node.name); + if (hasUnsupportedFeatures(containerApp)) { + const warning: string = containerApp.revisionsMode === KnownActiveRevisionsMode.Single ? + localize('confirmDeploySingle', 'Are you sure you want to deploy to "{0}"? This will overwrite the active revision and unsupported features in VS Code will be lost.', containerApp.name) : + localize('confirmDeployMultiple', 'Are you sure you want to deploy to "{0}"? Unsupported features in VS Code will be lost in the new revision.', containerApp.name); const items: MessageItem[] = [{ title: localize('deploy', 'Deploy') }]; await context.ui.showWarningMessage(warning, { modal: true, stepName: 'confirmDestructiveDeployment' }, ...items); } - const title: string = localize('updateImage', 'Update image in "{0}"', node.name); + const title: string = localize('updateImage', 'Update image in "{0}"', containerApp.name); const promptSteps: AzureWizardPromptStep[] = [new ContainerRegistryListStep(), new EnvironmentVariablesListStep()]; const executeSteps: AzureWizardExecuteStep[] = [new VerifyProvidersStep([webProvider])]; @@ -56,7 +58,7 @@ export async function deployImage(context: ITreeItemPickerContext & Partial { + const creatingRevision = localize('creatingRevision', 'Creating a new revision for container app "{0}"...', containerApp.name); + await ext.state.runWithTemporaryDescription(containerApp.id, localize('creating', 'Creating...'), async () => { await window.withProgress({ location: ProgressLocation.Notification, title: creatingRevision }, async (): Promise => { - node = nonNullValue(node); - const webClient: ContainerAppsAPIClient = await createContainerAppsAPIClient([wizardContext, node]); - ext.outputChannel.appendLog(creatingRevision); - node.data = await webClient.containerApps.beginCreateOrUpdateAndWait(node.resourceGroupName, node.name, containerAppEnvelope); - - void showContainerAppCreated(node, true); + const updatedContainerApp = await ContainerAppItem.Get(context, subscription, containerApp.resourceGroup, containerApp.name); + void showContainerAppCreated(updatedContainerApp, true); }); - }); - await node.refresh(context); + refreshContainerApp(containerApp.id); + }); } // check for any portal features that VS Code doesn't currently support -function hasUnsupportedFeatures(node: ContainerAppTreeItem): boolean { - if (node.data.template?.volumes) { +function hasUnsupportedFeatures(containerApp: ContainerApp): boolean { + if (containerApp.template?.volumes) { return true; - } else if (node.data.template?.containers) { - if (node.data.template.containers.length > 1) { + } else if (containerApp.template?.containers) { + if (containerApp.template.containers.length > 1) { return true; } - for (const container of node.data.template.containers) { + for (const container of containerApp.template.containers) { // NOTE: these are all arrays so if they are empty, this will still return true // but these should be undefined if not being utilized return !!container.probes || !!container.volumeMounts || !!container.args; diff --git a/src/commands/ingress/disableIngress.ts b/src/commands/ingress/disableIngress.ts new file mode 100644 index 000000000..97ec1ec98 --- /dev/null +++ b/src/commands/ingress/disableIngress.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { ContainerAppsItem } from "../../tree/ContainerAppsBranchDataProvider"; +import { localize } from '../../utils/localize'; +import { pickContainerApp } from "../../utils/pickContainerApp"; +import { updateIngressSettings } from "./updateIngressSettings"; + +export async function disableIngress(context: IActionContext, node?: ContainerAppsItem): Promise { + const { subscription, containerApp } = node ??= await pickContainerApp(context); + + await updateIngressSettings(context, { + ingress: null, + subscription: subscription, + containerApp: containerApp, + working: localize('disabling', 'Disabling ingress for container app "{0}"...', containerApp.name), + workCompleted: localize('disableCompleted', 'Disabled ingress for container app "{0}"', containerApp.name), + }); +} diff --git a/src/commands/ingress/editTargetPort.ts b/src/commands/ingress/editTargetPort.ts new file mode 100644 index 000000000..6e9ee19df --- /dev/null +++ b/src/commands/ingress/editTargetPort.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardPromptStep, IActionContext, nonNullProp, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { createSubscriptionContext } from "../../tree/ContainerAppsBranchDataProvider"; +import { IngressItem } from "../../tree/IngressItem"; +import { localize } from "../../utils/localize"; +import { pickContainerApp } from "../../utils/pickContainerApp"; +import { IContainerAppContext } from "../createContainerApp/IContainerAppContext"; +import { TargetPortStep } from "../createContainerApp/TargetPortStep"; +import { updateIngressSettings } from "./updateIngressSettings"; + +export async function editTargetPort(context: IActionContext, node?: IngressItem): Promise { + const { subscription, containerApp }: ContainerAppItem | IngressItem = node ?? await pickContainerApp(context); + + const title: string = localize('updateTargetPort', 'Update Target Port'); + const promptSteps: AzureWizardPromptStep[] = [new TargetPortStep()]; + + const wizardContext: IContainerAppContext = { + ...context, + ...createSubscriptionContext(subscription), + managedEnvironmentId: nonNullProp(containerApp, 'managedEnvironmentId'), + defaultPort: containerApp.configuration?.ingress?.targetPort + }; + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title, + promptSteps, + executeSteps: [] + }); + + await wizard.prompt(); + const ingress = nonNullValueAndProp(containerApp.configuration, 'ingress'); + ingress.targetPort = wizardContext.targetPort; + const working: string = localize('updatingTargetPort', 'Updating target port to {0}...', ingress.targetPort); + const workCompleted: string = localize('updatedTargetPort', 'Updated target port to {0}', ingress.targetPort); + await updateIngressSettings(context, { ingress, subscription, containerApp, working, workCompleted }); +} diff --git a/src/commands/ingress/enableIngress.ts b/src/commands/ingress/enableIngress.ts new file mode 100644 index 000000000..026657d8a --- /dev/null +++ b/src/commands/ingress/enableIngress.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Ingress } from "@azure/arm-appcontainers"; +import { AzureWizard, IActionContext, nonNullProp } from '@microsoft/vscode-azext-utils'; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ContainerAppModel } from "../../tree/ContainerAppItem"; +import { ContainerAppsItem, createSubscriptionContext } from "../../tree/ContainerAppsBranchDataProvider"; +import { localize } from '../../utils/localize'; +import { pickContainerApp } from "../../utils/pickContainerApp"; +import { EnableIngressStep } from '../createContainerApp/EnableIngressStep'; +import { IContainerAppContext } from '../createContainerApp/IContainerAppContext'; +import { updateIngressSettings } from "./updateIngressSettings"; + +export async function enableIngress(context: IActionContext, node?: ContainerAppsItem): Promise { + const { subscription, containerApp } = node ??= await pickContainerApp(context); + + await updateIngressSettings(context, { + ingress: await promptForIngressConfiguration(context, subscription, containerApp), + subscription: subscription, + containerApp: containerApp, + working: localize('enabling', 'Enabling ingress for container app "{0}"...', containerApp.name), + workCompleted: localize('enableCompleted', 'Enabled ingress for container app "{0}"', containerApp.name), + }); +} + +async function promptForIngressConfiguration(context: IActionContext, subscription: AzureSubscription, containerApp: ContainerAppModel): Promise { + const title: string = localize('enableIngress', 'Enable Ingress'); + const wizardContext: IContainerAppContext = { + ...context, + ...createSubscriptionContext(subscription), + managedEnvironmentId: nonNullProp(containerApp, 'managedEnvironmentId'), + }; + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title, + promptSteps: [new EnableIngressStep()] + }); + + wizardContext.enableIngress = true; + await wizard.prompt(); + + return { + targetPort: wizardContext.targetPort, + external: wizardContext.enableExternal, + transport: 'auto', + allowInsecure: false, + traffic: [ + { + "weight": 100, + "latestRevision": true + } + ], + }; +} diff --git a/src/commands/ingress/toggleIngressVisibility.ts b/src/commands/ingress/toggleIngressVisibility.ts new file mode 100644 index 000000000..5698c09c3 --- /dev/null +++ b/src/commands/ingress/toggleIngressVisibility.ts @@ -0,0 +1,20 @@ +import { IActionContext, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; +import { IngressConstants } from "../../constants"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { IngressItem } from "../../tree/IngressItem"; +import { localize } from "../../utils/localize"; +import { pickContainerApp } from "../../utils/pickContainerApp"; +import { updateIngressSettings } from "./updateIngressSettings"; + +export async function toggleIngressVisibility(context: IActionContext, node?: IngressItem | ContainerAppItem): Promise { + node ??= await pickContainerApp(context); + + const ingress = nonNullValueAndProp(node.containerApp.configuration, 'ingress'); + const warningPrompt = localize('visibilityWarning', 'This will change the ingress visibility from "{0}" to "{1}".', ingress.external ? IngressConstants.external : IngressConstants.internal, !ingress.external ? IngressConstants.external : IngressConstants.internal) + await context.ui.showWarningMessage(warningPrompt, { modal: true }, { title: localize('continue', 'Continue') }); + + ingress.external = !ingress.external; + const working: string = localize('updatingVisibility', 'Updating ingress visibility to "{0}"...', ingress.external ? IngressConstants.external : IngressConstants.internal); + const workCompleted: string = localize('updatedVisibility', 'Updated ingress visibility to "{0}"', ingress.external ? IngressConstants.external : IngressConstants.internal); + await updateIngressSettings(context, { ingress, subscription: node.subscription, containerApp: node.containerApp, working, workCompleted }); +} diff --git a/src/commands/ingress/updateIngressSettings.ts b/src/commands/ingress/updateIngressSettings.ts new file mode 100644 index 000000000..c03572777 --- /dev/null +++ b/src/commands/ingress/updateIngressSettings.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { Ingress } from "@azure/arm-appcontainers"; +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ProgressLocation, window } from "vscode"; +import { ext } from "../../extensionVariables"; +import { ContainerAppModel } from "../../tree/ContainerAppItem"; +import { updateContainerApp } from "../updateContainerApp"; + +export async function updateIngressSettings(context: IActionContext, + options: { + ingress: Ingress | null, + subscription: AzureSubscription, + containerApp: ContainerAppModel, + working: string, + workCompleted: string + }): Promise { + const { ingress, subscription, containerApp, working, workCompleted } = options; + + await window.withProgress({ location: ProgressLocation.Notification, title: working }, async (): Promise => { + ext.outputChannel.appendLog(working); + await updateContainerApp(context, subscription, containerApp, { configuration: { ingress: ingress as Ingress | undefined } }) + + void window.showInformationMessage(workCompleted); + ext.outputChannel.appendLog(workCompleted); + }); + + ext.state.notifyChildrenChanged(containerApp.managedEnvironmentId) +} diff --git a/src/commands/ingressCommands.ts b/src/commands/ingressCommands.ts deleted file mode 100644 index 986e08dfe..000000000 --- a/src/commands/ingressCommands.ts +++ /dev/null @@ -1,138 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Ingress } from "@azure/arm-appcontainers"; -import { AzureWizard, AzureWizardPromptStep, GenericTreeItem, IActionContext } from '@microsoft/vscode-azext-utils'; -import { ProgressLocation, window } from 'vscode'; -import { IngressConstants, rootFilter } from '../constants'; -import { ext } from '../extensionVariables'; -import { IngressDisabledTreeItem, IngressTreeItem } from '../tree/IngressTreeItem'; -import { localize } from '../utils/localize'; -import { nonNullValueAndProp } from '../utils/nonNull'; -import { EnableIngressStep } from './createContainerApp/EnableIngressStep'; -import { IContainerAppContext } from './createContainerApp/IContainerAppContext'; -import { TargetPortStep } from './createContainerApp/TargetPortStep'; -import { updateContainerApp } from "./updateContainerApp"; - -export async function toggleIngress(context: IActionContext, node?: IngressTreeItem | IngressDisabledTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: [IngressTreeItem.contextValue, IngressDisabledTreeItem.contextValue] - }); - } - - let ingress: Ingress | null = {}; - - if (node instanceof IngressTreeItem) { - // PATCH requires ingress be set to null exclusively to be picked up since undefined means there was no update - ingress = null; - } else { - const title: string = localize('enableIngress', 'Enable Ingress'); - const promptSteps: AzureWizardPromptStep[] = [new EnableIngressStep()]; - - const wizardContext: IContainerAppContext = { ...context, ...node.subscription, managedEnvironmentId: node.parent.managedEnvironmentId }; - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title, - promptSteps, - executeSteps: [] - }); - - wizardContext.enableIngress = true; - await wizard.prompt(); - - ingress = { - targetPort: wizardContext.targetPort, - external: wizardContext.enableExternal, - transport: 'auto', - allowInsecure: false, - traffic: [ - { - "weight": 100, - "latestRevision": true - } - ], - }; - } - - const name = node.parent.name; - const working = node instanceof IngressTreeItem ? localize('disabling', 'Disabling ingress for container app "{0}"...', name) : localize('enabling', 'Enabling ingress for container app "{0}"...', name); - const workCompleted = node instanceof IngressTreeItem ? localize('disableCompleted', 'Disabled ingress for container app "{0}"', name) : localize('enableCompleted', 'Enabled ingress for container app "{0}"', name); - - await updateIngressSettings(context, { ingress, node, working, workCompleted }); -} - -export async function editTargetPort(context: IActionContext, target?: IngressTreeItem | GenericTreeItem): Promise { - if (!target) { - target = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: IngressTreeItem.contextValue - }); - } - - // GenericTreeItem will be a targetPort node - const node: IngressTreeItem = target instanceof IngressTreeItem ? target : target.parent as IngressTreeItem; - - const title: string = localize('updateTargetPort', 'Update Target Port'); - const promptSteps: AzureWizardPromptStep[] = [new TargetPortStep()]; - - const wizardContext: IContainerAppContext = { - ...context, - ...node.subscription, - managedEnvironmentId: node.parent.managedEnvironmentId, - defaultPort: node.data.targetPort - }; - - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title, - promptSteps, - executeSteps: [] - }); - - await wizard.prompt(); - const ingress = nonNullValueAndProp(node.parent.data.configuration, 'ingress'); - ingress.targetPort = wizardContext.targetPort; - const working: string = localize('updatingTargetPort', 'Updating target port to {0}...', ingress.targetPort); - const workCompleted: string = localize('updatedTargetPort', 'Updated target port to {0}', ingress.targetPort); - await updateIngressSettings(context, { ingress, node, working, workCompleted }); -} - -export async function toggleIngressVisibility(context: IActionContext, node?: IngressTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: IngressTreeItem.contextValue - }); - } - - const ingress = nonNullValueAndProp(node.parent.data.configuration, 'ingress'); - const warningPrompt = localize('visibilityWarning', 'This will change the ingress visibility from "{0}" to "{1}".', ingress.external ? IngressConstants.external : IngressConstants.internal, !ingress.external ? IngressConstants.external : IngressConstants.internal) - await context.ui.showWarningMessage(warningPrompt, { modal: true }, { title: localize('continue', 'Continue') }); - - ingress.external = !ingress.external; - const working: string = localize('updatingVisibility', 'Updating ingress visibility to "{0}"...', ingress.external ? IngressConstants.external : IngressConstants.internal); - const workCompleted: string = localize('updatedVisibility', 'Updated ingress visibility to "{0}"', ingress.external ? IngressConstants.external : IngressConstants.internal); - await updateIngressSettings(context, { ingress, node, working, workCompleted }); -} - -async function updateIngressSettings(context: IActionContext, - options: { - ingress: Ingress | null, - node: IngressTreeItem | IngressDisabledTreeItem, - working: string, - workCompleted: string - }): Promise { - const { ingress, node, working, workCompleted } = options; - - await window.withProgress({ location: ProgressLocation.Notification, title: working }, async (): Promise => { - ext.outputChannel.appendLog(working); - await updateContainerApp(context, node.parent, { configuration: { ingress: ingress as Ingress | undefined } }) - - void window.showInformationMessage(workCompleted); - ext.outputChannel.appendLog(workCompleted); - }); - - await node.parent.refresh(context); -} diff --git a/src/commands/openConsoleInPortal.ts b/src/commands/openConsoleInPortal.ts new file mode 100644 index 000000000..56fbac836 --- /dev/null +++ b/src/commands/openConsoleInPortal.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { commands } from "vscode"; +import { ContainerAppItem } from "../tree/ContainerAppItem"; +import { createPortalUrl } from "../utils/createPortalUrl"; +import { pickContainerApp } from "../utils/pickContainerApp"; + +export async function openConsoleInPortal(context: IActionContext, node?: ContainerAppItem): Promise { + node ??= await pickContainerApp(context); + await commands.executeCommand('azureResourceGroups.openInPortal', { + portalUrl: createPortalUrl(node.subscription, `${node.containerApp.id}/console`), + }); +} diff --git a/src/commands/openInPortal.ts b/src/commands/openInPortal.ts deleted file mode 100644 index f2a7b7217..000000000 --- a/src/commands/openInPortal.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as azUtil from '@microsoft/vscode-azext-azureutils'; -import { AzExtTreeItem, IActionContext } from '@microsoft/vscode-azext-utils'; - -export async function openInPortal(_context: IActionContext, node: AzExtTreeItem): Promise { - await azUtil.openInPortal(node, node.id ?? node.fullId); -} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index d862d7a9c..fc48a30f1 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -3,56 +3,52 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as azUtil from '@microsoft/vscode-azext-azureutils'; -import { IActionContext, registerCommand, registerErrorHandler, registerReportIssueCommand } from '@microsoft/vscode-azext-utils'; -import { commands } from 'vscode'; -import { rootFilter } from '../constants'; -import { ext } from '../extensionVariables'; -import { ContainerAppTreeItem } from '../tree/ContainerAppTreeItem'; -import { ManagedEnvironmentTreeItem } from '../tree/ManagedEnvironmentTreeItem'; -import { RevisionTreeItem } from '../tree/RevisionTreeItem'; -import { browse } from './browse'; +import { registerCommandWithTreeNodeUnwrapping, registerErrorHandler, registerReportIssueCommand } from '@microsoft/vscode-azext-utils'; +import { browseContainerAppNode } from './browseContainerApp'; import { chooseRevisionMode } from './chooseRevisionMode'; import { createContainerApp } from './createContainerApp/createContainerApp'; import { createManagedEnvironment } from './createManagedEnvironment/createManagedEnvironment'; -import { deleteNode } from './deleteNode'; +import { deleteContainerApp } from './deleteContainerApp/deleteContainerApp'; +import { deleteManagedEnvironment } from './deleteManagedEnvironment/deleteManagedEnvironment'; import { deployImage } from './deployImage/deployImage'; -import { editTargetPort, toggleIngress, toggleIngressVisibility } from './ingressCommands'; -import { openInPortal } from './openInPortal'; -import { changeRevisionActiveState } from './revisionCommands/changeRevisionActiveState'; +import { disableIngress } from './ingress/disableIngress'; +import { editTargetPort } from './ingress/editTargetPort'; +import { enableIngress } from './ingress/enableIngress'; +import { toggleIngressVisibility } from './ingress/toggleIngressVisibility'; +import { openConsoleInPortal } from './openConsoleInPortal'; +import { activateRevision } from './revisionCommands/activateRevision'; +import { deactivateRevision } from './revisionCommands/deactivateRevision'; +import { restartRevision } from './revisionCommands/restartRevision'; import { addScaleRule } from './scaling/addScaleRule/addScaleRule'; import { editScalingRange } from './scaling/editScalingRange'; -import { viewProperties } from './viewProperties'; export function registerCommands(): void { - registerCommand('containerApps.viewProperties', viewProperties); - registerCommand('containerApps.openInPortal', openInPortal); - registerCommand('containerApps.selectSubscriptions', () => commands.executeCommand('azure-account.selectSubscriptions')); - registerCommand('containerApps.browse', browse); - registerCommand('containerApps.createManagedEnvironment', createManagedEnvironment); - registerCommand('containerApps.createContainerApp', createContainerApp); - registerCommand('containerApps.deployImage', deployImage); - registerCommand('containerApps.deleteManagedEnvironment', async (context: IActionContext, node?: ManagedEnvironmentTreeItem) => await deleteNode(context, ManagedEnvironmentTreeItem.contextValueRegExp, node)); - registerCommand('containerApps.deleteContainerApp', async (context: IActionContext, node?: ContainerAppTreeItem) => await deleteNode(context, ContainerAppTreeItem.contextValueRegExp, node)); - registerCommand('containerApps.enableIngress', toggleIngress); - registerCommand('containerApps.disableIngress', toggleIngress); - registerCommand('containerApps.toggleVisibility', toggleIngressVisibility); - registerCommand('containerApps.editTargetPort', editTargetPort); - registerCommand('containerApps.chooseRevisionMode', chooseRevisionMode); - registerCommand('containerApps.activateRevision', async (context: IActionContext, node?: RevisionTreeItem) => await changeRevisionActiveState(context, 'activate', node)); - registerCommand('containerApps.deactivateRevision', async (context: IActionContext, node?: RevisionTreeItem) => await changeRevisionActiveState(context, 'deactivate', node)); - registerCommand('containerApps.restartRevision', async (context: IActionContext, node?: RevisionTreeItem) => await changeRevisionActiveState(context, 'restart', node)); - registerCommand('containerApps.openConsoleInPortal', async (context: IActionContext, node?: ContainerAppTreeItem) => { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: ContainerAppTreeItem.contextValueRegExp - }); - } - await azUtil.openInPortal(node, `${node.id}/console`); - }); - registerCommand('containerApps.editScalingRange', editScalingRange); - registerCommand('containerApps.addScaleRule', addScaleRule); + // managed environments + registerCommandWithTreeNodeUnwrapping('containerApps.createManagedEnvironment', createManagedEnvironment); + registerCommandWithTreeNodeUnwrapping('containerApps.deleteManagedEnvironment', deleteManagedEnvironment); + + // container apps + registerCommandWithTreeNodeUnwrapping('containerApps.createContainerApp', createContainerApp); + registerCommandWithTreeNodeUnwrapping('containerApps.deleteContainerApp', deleteContainerApp); + registerCommandWithTreeNodeUnwrapping('containerApps.deployImage', deployImage); + registerCommandWithTreeNodeUnwrapping('containerApps.openConsoleInPortal', openConsoleInPortal); + registerCommandWithTreeNodeUnwrapping('containerApps.browse', browseContainerAppNode); + + // ingress + registerCommandWithTreeNodeUnwrapping('containerApps.enableIngress', enableIngress); + registerCommandWithTreeNodeUnwrapping('containerApps.disableIngress', disableIngress); + registerCommandWithTreeNodeUnwrapping('containerApps.toggleVisibility', toggleIngressVisibility); + registerCommandWithTreeNodeUnwrapping('containerApps.editTargetPort', editTargetPort); + + // revisions + registerCommandWithTreeNodeUnwrapping('containerApps.chooseRevisionMode', chooseRevisionMode); + registerCommandWithTreeNodeUnwrapping('containerApps.activateRevision', activateRevision); + registerCommandWithTreeNodeUnwrapping('containerApps.deactivateRevision', deactivateRevision); + registerCommandWithTreeNodeUnwrapping('containerApps.restartRevision', restartRevision); + + // scaling + registerCommandWithTreeNodeUnwrapping('containerApps.editScalingRange', editScalingRange); + registerCommandWithTreeNodeUnwrapping('containerApps.addScaleRule', addScaleRule); // Suppress "Report an Issue" button for all errors in favor of the command registerErrorHandler(c => c.errorHandling.suppressReportIssue = true); diff --git a/src/commands/revisionCommands/activateRevision.ts b/src/commands/revisionCommands/activateRevision.ts new file mode 100644 index 000000000..8f76470ac --- /dev/null +++ b/src/commands/revisionCommands/activateRevision.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { RevisionItem } from "../../tree/RevisionItem"; +import { executeRevisionOperation } from "./changeRevisionActiveState"; + +export function activateRevision(context: IActionContext, node?: ContainerAppItem | RevisionItem): Promise { + return executeRevisionOperation(context, node, 'activateRevision'); +} diff --git a/src/commands/revisionCommands/changeRevisionActiveState.ts b/src/commands/revisionCommands/changeRevisionActiveState.ts index f5d522e57..de64cfbc5 100644 --- a/src/commands/revisionCommands/changeRevisionActiveState.ts +++ b/src/commands/revisionCommands/changeRevisionActiveState.ts @@ -5,45 +5,28 @@ import { ContainerAppsAPIClient } from "@azure/arm-appcontainers"; import { IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; -import { rootFilter } from "../../constants"; import { ext } from "../../extensionVariables"; -import { ContainerAppTreeItem } from '../../tree/ContainerAppTreeItem'; -import { RevisionTreeItem } from "../../tree/RevisionTreeItem"; -import { createContainerAppsAPIClient } from "../../utils/azureClients"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { RevisionItem } from "../../tree/RevisionItem"; +import { createContainerAppsClient } from "../../utils/azureClients"; import { localize } from "../../utils/localize"; +import { pickContainerApp } from "../../utils/pickContainerApp"; -export async function changeRevisionActiveState(context: IActionContext, command: 'activate' | 'deactivate' | 'restart', node?: ContainerAppTreeItem | RevisionTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: ContainerAppTreeItem.contextValueRegExp - }); - } +export async function executeRevisionOperation(context: IActionContext, node: ContainerAppItem | RevisionItem | undefined, operation: RevisionOperation): Promise { + const item = node ?? await pickContainerApp(context); - const containerAppName: string = node instanceof RevisionTreeItem ? node.parent.parent.name : node.name; - const revisionName: string = node instanceof RevisionTreeItem ? node.name : nonNullProp(node.data, 'latestRevisionName'); - const resourceGroupName: string = node instanceof RevisionTreeItem ? node.parent.parent.resourceGroupName : node.resourceGroupName; - - const appClient: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, node]); - - const temporaryDescriptions = { - 'activate': localize('activating', 'Activating...'), - 'deactivate': localize('deactivating', 'Deactivating...'), - 'restart': localize('restarting', 'Restarting...'), - } - await node.runWithTemporaryDescription(context, temporaryDescriptions[command], async () => { - switch (command) { - case 'activate': - await appClient.containerAppsRevisions.activateRevision(resourceGroupName, containerAppName, revisionName); - break; - case 'deactivate': - await appClient.containerAppsRevisions.deactivateRevision(resourceGroupName, containerAppName, revisionName); - break; - case 'restart': - await appClient.containerAppsRevisions.restartRevision(resourceGroupName, containerAppName, revisionName); - break; - } + await ext.state.runWithTemporaryDescription(item.id, revisionOperationDescriptions[operation], async () => { + const appClient: ContainerAppsAPIClient = await createContainerAppsClient(context, item.subscription); + const revisionName: string = item instanceof RevisionItem ? nonNullProp(item.revision, 'name') : nonNullProp(item.containerApp, 'latestRevisionName'); + await appClient.containerAppsRevisions[operation](item.containerApp.resourceGroup, item.containerApp.name, revisionName); + ext.state.notifyChildrenChanged(item.containerApp.id); }); - - await node.refresh(context); } + +const revisionOperationDescriptions = { + activateRevision: localize('activating', 'Activating...'), + deactivateRevision: localize('deactivating', 'Deactivating...'), + restartRevision: localize('restarting', 'Restarting...'), +} satisfies Partial>; + +export type RevisionOperation = keyof typeof revisionOperationDescriptions; diff --git a/src/commands/revisionCommands/deactivateRevision.ts b/src/commands/revisionCommands/deactivateRevision.ts new file mode 100644 index 000000000..a6ebb9f76 --- /dev/null +++ b/src/commands/revisionCommands/deactivateRevision.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { RevisionItem } from "../../tree/RevisionItem"; +import { executeRevisionOperation } from "./changeRevisionActiveState"; + +export function deactivateRevision(context: IActionContext, node?: ContainerAppItem | RevisionItem): Promise { + return executeRevisionOperation(context, node, 'deactivateRevision'); +} diff --git a/src/commands/revisionCommands/restartRevision.ts b/src/commands/revisionCommands/restartRevision.ts new file mode 100644 index 000000000..85905e779 --- /dev/null +++ b/src/commands/revisionCommands/restartRevision.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { RevisionItem } from "../../tree/RevisionItem"; +import { executeRevisionOperation } from "./changeRevisionActiveState"; + +export function restartRevision(context: IActionContext, node?: ContainerAppItem | RevisionItem): Promise { + return executeRevisionOperation(context, node, 'restartRevision'); +} diff --git a/src/commands/scaling/addScaleRule/AddScaleRuleStep.ts b/src/commands/scaling/addScaleRule/AddScaleRuleStep.ts index eb60ef0d1..afdb91487 100644 --- a/src/commands/scaling/addScaleRule/AddScaleRuleStep.ts +++ b/src/commands/scaling/addScaleRule/AddScaleRuleStep.ts @@ -19,15 +19,15 @@ export class AddScaleRuleStep extends AzureWizardExecuteStep { public hideStepCount: boolean = true; - containerApp: ContainerApp | undefined; - scaleRules: ScaleRule[] | undefined; public async prompt(context: IAddScaleRuleWizardContext): Promise { - this.containerApp = context.containerApp.data; - this.scaleRules = context.scaleRuleGroup.data; context.ruleName = (await context.ui.showInputBox({ prompt: localize('scaleRuleNamePrompt', 'Enter a name for the new scale rule.'), - validateInput: (value: string | undefined): string | undefined => this.validateInput(value) + validateInput: (name: string | undefined): string | undefined => { + return validateScaleRuleInput(name, context.containerApp?.name, context.scaleRules); + } })).trim(); } public shouldPrompt(context: IAddScaleRuleWizardContext): boolean { return context.ruleName === undefined; } +} - private validateInput(name: string | undefined): string | undefined { - name = name ? name.trim() : ''; +function validateScaleRuleInput(name: string | undefined, containerAppName: string, scaleRules: ScaleRule[]): string | undefined { + name = name ? name.trim() : ''; - if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(name)) { - return localize('invalidChar', `A name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character.`); - } + if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(name)) { + return localize('invalidChar', `A name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character.`); + } - const scaleRuleExists: boolean = !!this.scaleRules?.some((rule) => { - return rule?.name?.length && rule?.name === name; - }); - if (scaleRuleExists) { - return localize('scaleRuleExists', 'The scale rule "{0}" already exists in container app "{1}". Please enter a unique name.', name, this.containerApp?.name); - } - return undefined; + const scaleRuleExists: boolean = !!scaleRules?.some((rule) => { + return rule?.name?.length && rule?.name === name; + }); + if (scaleRuleExists) { + return localize('scaleRuleExists', 'The scale rule "{0}" already exists in container app "{1}". Please enter a unique name.', name, containerAppName); } + return undefined; } diff --git a/src/commands/scaling/addScaleRule/addScaleRule.ts b/src/commands/scaling/addScaleRule/addScaleRule.ts index 2517c7f57..d8b5bf46e 100644 --- a/src/commands/scaling/addScaleRule/addScaleRule.ts +++ b/src/commands/scaling/addScaleRule/addScaleRule.ts @@ -3,21 +3,69 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from "@microsoft/vscode-azext-utils"; -import { rootFilter } from "../../../constants"; +import { ContainerAppsAPIClient, KnownActiveRevisionsMode, Revision } from "@azure/arm-appcontainers"; +import { uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizard, IActionContext, IAzureQuickPickItem, nonNullProp, nonNullValue } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; import { ext } from "../../../extensionVariables"; -import { ContainerAppTreeItem } from "../../../tree/ContainerAppTreeItem"; -import { ScaleRuleGroupTreeItem } from "../../../tree/ScaleRuleGroupTreeItem"; -import { treeUtils } from "../../../utils/treeUtils"; - -export async function addScaleRule(context: IActionContext, node?: ScaleRuleGroupTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: new RegExp(ScaleRuleGroupTreeItem.contextValue) - }); +import { ContainerAppItem, ContainerAppModel } from "../../../tree/ContainerAppItem"; +import { ScaleRuleGroupItem } from "../../../tree/scaling/ScaleRuleGroupItem"; +import { createContainerAppsClient } from "../../../utils/azureClients"; +import { localize } from "../../../utils/localize"; +import { pickContainerApp } from "../../../utils/pickContainerApp"; +import { AddScaleRuleStep } from "./AddScaleRuleStep"; +import { IAddScaleRuleWizardContext } from "./IAddScaleRuleWizardContext"; +import { ScaleRuleNameStep } from "./ScaleRuleNameStep"; +import { ScaleRuleTypeStep } from "./ScaleRuleTypeStep"; + +export async function addScaleRule(context: IActionContext, node?: ScaleRuleGroupItem): Promise { + const { subscription, containerApp, revision } = node ?? await getContainerAppAndRevision(context); + + const scale = nonNullValue(revision?.template?.scale); + const wizardContext: IAddScaleRuleWizardContext = { + ...context, + scale, + subscription, + containerApp, + scaleRules: scale.rules ?? [], + }; + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title: localize('addScaleRuleTitle', 'Add Scale Rule'), + promptSteps: [new ScaleRuleNameStep(), new ScaleRuleTypeStep()], + executeSteps: [new AddScaleRuleStep()], + showLoadingPrompt: true + }); + + await wizard.prompt(); + await wizard.execute(); + ext.state.notifyChildrenChanged(containerApp.managedEnvironmentId); +} + +export async function getContainerAppAndRevision(context: IActionContext): Promise<{ containerApp: ContainerAppModel, revision: Revision, subscription: AzureSubscription }> { + const containerAppItem = await pickContainerApp(context); + const revision = await pickRevision(context, containerAppItem); + return { containerApp: containerAppItem.containerApp, revision, subscription: containerAppItem.subscription }; +} + +/** + * If the container app is in single revision mode, returns the latest revision. Otherwise, prompts the user to pick a revision. + */ +async function pickRevision(context: IActionContext, node: ContainerAppItem): Promise { + const client: ContainerAppsAPIClient = await createContainerAppsClient(context, node.subscription); + + if (node.containerApp.revisionsMode === KnownActiveRevisionsMode.Single) { + return await client.containerAppsRevisions.getRevision(node.containerApp.resourceGroup, node.containerApp.name, nonNullProp(node.containerApp, 'latestRevisionName')); + } else { + async function getPicks(): Promise[]> { + const revisions = await uiUtils.listAllIterator(client.containerAppsRevisions.listRevisions(node.containerApp.resourceGroup, node.containerApp.name)); + return revisions.map(r => ({ + label: nonNullProp(r, 'name'), + data: r, + })); + } + return (await context.ui.showQuickPick>(getPicks(), { + canPickMany: false, + })).data; } - const containerApp: ContainerAppTreeItem = treeUtils.findNearestParent(node, ContainerAppTreeItem.contextValue); - await node.createChild(context); - await containerApp.refresh(context); } diff --git a/src/commands/scaling/addScaleRule/queue/QueueAuthSecretStep.ts b/src/commands/scaling/addScaleRule/queue/QueueAuthSecretStep.ts index 2c64a8e80..242ca4a88 100644 --- a/src/commands/scaling/addScaleRule/queue/QueueAuthSecretStep.ts +++ b/src/commands/scaling/addScaleRule/queue/QueueAuthSecretStep.ts @@ -6,13 +6,14 @@ import { Secret } from '@azure/arm-appcontainers'; import { AzureWizardPromptStep, nonNullProp } from '@microsoft/vscode-azext-utils'; import { QuickPickItem } from 'vscode'; +import { getContainerEnvelopeWithSecrets } from '../../../../tree/ContainerAppItem'; import { localize } from '../../../../utils/localize'; import { IAddScaleRuleWizardContext } from '../IAddScaleRuleWizardContext'; export class QueueAuthSecretStep extends AzureWizardPromptStep { public async prompt(context: IAddScaleRuleWizardContext): Promise { const placeHolder: string = localize('chooseSecretRef', 'Choose a secret reference'); - const containerAppWithSecrets = await context.containerApp.getContainerEnvelopeWithSecrets(context); + const containerAppWithSecrets = await getContainerEnvelopeWithSecrets(context, context.subscription, context.containerApp); const secrets: Secret[] | undefined = containerAppWithSecrets.configuration.secrets; if (!secrets?.length) { const noSecrets: string = localize('noSecretsFound', 'No secrets were found. Create a secret to proceed.'); diff --git a/src/commands/scaling/editScalingRange.ts b/src/commands/scaling/editScalingRange.ts index 539790c2d..60c8bee9a 100644 --- a/src/commands/scaling/editScalingRange.ts +++ b/src/commands/scaling/editScalingRange.ts @@ -3,26 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { IActionContext, nonNullValue } from "@microsoft/vscode-azext-utils"; import { ProgressLocation, window } from "vscode"; -import { rootFilter } from "../../constants"; import { ext } from "../../extensionVariables"; -import { ContainerAppTreeItem } from "../../tree/ContainerAppTreeItem"; -import { RevisionTreeItem } from "../../tree/RevisionTreeItem"; -import { ScaleTreeItem } from "../../tree/ScaleTreeItem"; +import { refreshContainerApp } from "../../tree/ContainerAppItem"; +import { ScaleItem } from "../../tree/scaling/ScaleItem"; import { localize } from "../../utils/localize"; import { updateContainerApp } from "../updateContainerApp"; +import { getContainerAppAndRevision } from "./addScaleRule/addScaleRule"; -export async function editScalingRange(context: IActionContext, node?: ScaleTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: new RegExp(ScaleTreeItem.contextValue) - }); - } +export async function editScalingRange(context: IActionContext, node?: ScaleItem): Promise { + const { containerApp, revision, subscription } = node ?? await getContainerAppAndRevision(context); + const scale = nonNullValue(revision?.template?.scale); const prompt: string = localize('editScalingRange', 'Set the range of application replicas that get created in response to a scale rule. Set any range within the minimum of 0 and the maximum of 10 replicas'); - const value: string = `${node.minReplicas}-${node.maxReplicas}`; + const value: string = `${scale.minReplicas ?? 0}-${scale.maxReplicas ?? 0}`; const range = await context.ui.showInputBox({ prompt, value, @@ -30,12 +25,11 @@ export async function editScalingRange(context: IActionContext, node?: ScaleTree }); const [min, max] = range.split('-').map(range => Number(range)); - const containerApp = node.parent instanceof RevisionTreeItem ? node.parent.parent.parent as ContainerAppTreeItem : node.parent as ContainerAppTreeItem; const updating = localize('updatingRevision', 'Updating scale rule setting of "{0}" to min/max replicas of {1}-{2}...', containerApp.name, min, max); const updated = localize('updatedRevision', 'Updated scale rule setting of "{0}" to min/max replicas of {1}-{2}.', containerApp.name, min, max); - const template = containerApp?.data?.template || {}; + const template = containerApp?.template || {}; template.scale ||= {}; template.scale.minReplicas = min; @@ -43,9 +37,8 @@ export async function editScalingRange(context: IActionContext, node?: ScaleTree await window.withProgress({ location: ProgressLocation.Notification, title: updating }, async (): Promise => { ext.outputChannel.appendLog(updating); - await updateContainerApp(context, containerApp, { template }) - - await containerApp?.refresh(context); + await updateContainerApp(context, subscription, containerApp, { template }) + refreshContainerApp(containerApp.id); void window.showInformationMessage(updated); ext.outputChannel.appendLog(updated); }); @@ -68,4 +61,3 @@ async function validateInput(range: string | undefined): Promise): Promise { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, node]); - const resourceGroupName = node.resourceGroupName; - const name = node.name; - const updatedApp: ContainerApp = { ...updatedSetting, location: node.data.location }; +export async function updateContainerApp(context: IActionContext, subscription: AzureSubscription, containerApp: ContainerApp, updatedSetting: Omit): Promise { + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, createSubscriptionContext(subscription)]); + const resourceGroupName = getResourceGroupFromId(nonNullProp(containerApp, 'id')); + const name = nonNullProp(containerApp, 'name'); + const updatedApp: ContainerApp = { ...updatedSetting, location: containerApp.location }; await client.containerApps.beginUpdateAndWait(resourceGroupName, name, updatedApp); } diff --git a/src/commands/viewProperties.ts b/src/commands/viewProperties.ts deleted file mode 100644 index f858a1178..000000000 --- a/src/commands/viewProperties.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IActionContext, openReadOnlyJson } from '@microsoft/vscode-azext-utils'; -import { azResourceRegExp, rootFilter } from '../constants'; -import { ext } from '../extensionVariables'; -import { IAzureResourceTreeItem } from '../tree/IAzureResourceTreeItem'; -import { localize } from '../utils/localize'; -import { nonNullProp } from '../utils/nonNull'; - -export async function viewProperties(context: IActionContext, node?: IAzureResourceTreeItem): Promise { - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: rootFilter, - expectedChildContextValue: azResourceRegExp - }); - } - - if (!node.data) { - if (node.getDataImpl) { - await node.getDataImpl(); - } else { - throw new Error(localize('viewProperties.noData', 'No data exists on resource "{0}"', node.label)); - } - } - - await openReadOnlyJson(node, nonNullProp(node, 'data')); -} diff --git a/src/extension.ts b/src/extension.ts index 2d624211e..c0d3ec5cf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,17 +6,15 @@ 'use strict'; import { registerAzureUtilsExtensionVariables } from '@microsoft/vscode-azext-azureutils'; -import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtOutputChannel, createExperimentationService, IActionContext, registerUIExtensionVariables } from '@microsoft/vscode-azext-utils'; -import { AzureExtensionApi, AzureExtensionApiProvider } from '@microsoft/vscode-azext-utils/api'; +import { AzExtResourceType, callWithTelemetryAndErrorHandling, createAzExtOutputChannel, createExperimentationService, IActionContext, registerUIExtensionVariables } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; -import { revealTreeItem } from './commands/api/revealTreeItem'; import { registerCommands } from './commands/registerCommands'; -import { managedEnvironmentsAppProvider } from './constants'; -import { ContainerAppsResolver } from './ContainerAppsResolver'; import { ext } from './extensionVariables'; import { getResourceGroupsApi } from './getExtensionApi'; +import { ContainerAppsBranchDataProvider } from './tree/ContainerAppsBranchDataProvider'; +import { TreeItemStateStore } from './tree/TreeItemState'; -export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { +export async function activateInternal(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }, ignoreBundle?: boolean): Promise { ext.context = context; ext.ignoreBundle = ignoreBundle; ext.outputChannel = createAzExtOutputChannel('Azure Container Apps', ext.prefix); @@ -32,14 +30,11 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta registerCommands(); ext.experimentationService = await createExperimentationService(context); - ext.rgApi = await getResourceGroupsApi(); - ext.rgApi.registerApplicationResourceResolver(managedEnvironmentsAppProvider, new ContainerAppsResolver()); + ext.state = new TreeItemStateStore(); + ext.rgApiV2 = await getResourceGroupsApi(); + ext.branchDataProvider = new ContainerAppsBranchDataProvider(); + ext.rgApiV2.resources.registerAzureResourceBranchDataProvider(AzExtResourceType.ContainerAppsEnvironment, ext.branchDataProvider); }); - - return createApiProvider([{ - revealTreeItem, - apiVersion: '1.0.0' - }]); } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index bffdf1729..216483b27 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -5,7 +5,10 @@ import { IAzExtOutputChannel, IExperimentationServiceAdapter } from "@microsoft/vscode-azext-utils"; import { AzureHostExtensionApi } from "@microsoft/vscode-azext-utils/hostapi"; +import { AzureResourcesApi } from "@microsoft/vscode-azext-utils/hostapi.v2"; import { ExtensionContext } from "vscode"; +import { ContainerAppsBranchDataProvider } from "./tree/ContainerAppsBranchDataProvider"; +import { TreeItemStateStore } from "./tree/TreeItemState"; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -17,4 +20,8 @@ export namespace ext { export const prefix: string = 'containerApps'; export let experimentationService: IExperimentationServiceAdapter; export let rgApi: AzureHostExtensionApi; + export let rgApiV2: AzureResourcesApi; + + export let state: TreeItemStateStore; + export let branchDataProvider: ContainerAppsBranchDataProvider; } diff --git a/src/getExtensionApi.ts b/src/getExtensionApi.ts index 231689ddf..bb81fad20 100644 --- a/src/getExtensionApi.ts +++ b/src/getExtensionApi.ts @@ -3,28 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getExtensionExports } from "@microsoft/vscode-azext-utils"; import { AzureExtensionApiProvider } from "@microsoft/vscode-azext-utils/api"; -import { AzureHostExtensionApi } from "@microsoft/vscode-azext-utils/hostapi"; -import { Extension, extensions } from "vscode"; +import { AzureResourcesApi } from "@microsoft/vscode-azext-utils/hostapi.v2"; import { localize } from "./utils/localize"; -export async function getApiExport(extensionId: string): Promise { - const extension: Extension | undefined = extensions.getExtension(extensionId); - if (extension) { - if (!extension.isActive) { - await extension.activate(); - } - - return extension.exports; - } - - return undefined; -} - -export async function getResourceGroupsApi(): Promise { - const rgApiProvider = await getApiExport('ms-azuretools.vscode-azureresourcegroups'); +export async function getResourceGroupsApi(): Promise { + const rgApiProvider = await getExtensionExports('ms-azuretools.vscode-azureresourcegroups'); if (rgApiProvider) { - return rgApiProvider.getApi('0.0.1'); + return rgApiProvider.getApi('2.0.0', { + extensionId: 'ms-azuretools.vscode-azurecontainerapps', + }); } else { throw new Error(localize('noResourceGroupExt', 'Could not find the Azure Resource Groups extension')); } diff --git a/src/tree/AzureAccountTreeItem.ts b/src/tree/AzureAccountTreeItem.ts deleted file mode 100644 index 861096423..000000000 --- a/src/tree/AzureAccountTreeItem.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils'; -import { ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { SubscriptionTreeItem } from './SubscriptionTreeItem'; - -export class AzureAccountTreeItem extends AzureAccountTreeItemBase { - public constructor(testAccount?: {}) { - super(undefined, testAccount); - } - - public createSubscriptionTreeItem(root: ISubscriptionContext): SubscriptionTreeItem { - return new SubscriptionTreeItem(this, root); - } -} diff --git a/src/tree/ContainerAppItem.ts b/src/tree/ContainerAppItem.ts new file mode 100644 index 000000000..4b2b0a7f1 --- /dev/null +++ b/src/tree/ContainerAppItem.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { ContainerApp, ContainerAppsAPIClient, KnownActiveRevisionsMode } from "@azure/arm-appcontainers"; +import { getResourceGroupFromId, uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizard, callWithTelemetryAndErrorHandling, DeleteConfirmationStep, IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription, ViewPropertiesModel } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { EventEmitter, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; +import { DeleteAllContainerAppsStep } from "../commands/deleteContainerApp/DeleteAllContainerAppsStep"; +import { IDeleteContainerAppWizardContext } from "../commands/deleteContainerApp/IDeleteContainerAppWizardContext"; +import { IDeletable } from "../commands/deleteNode"; +import { ext } from "../extensionVariables"; +import { createActivityContext } from "../utils/activityUtils"; +import { createContainerAppsAPIClient, createContainerAppsClient } from "../utils/azureClients"; +import { createPortalUrl } from "../utils/createPortalUrl"; +import { localize } from "../utils/localize"; +import { treeUtils } from "../utils/treeUtils"; +import { ContainerAppsItem, createSubscriptionContext, TreeElementBase } from "./ContainerAppsBranchDataProvider"; +import { createDaprDisabledItem, DaprEnabledItem } from "./DaprItem"; +import { IngressDisabledItem, IngressItem } from "./IngressItem"; +import { LogsItem } from "./LogsItem"; +import { RevisionsItem } from "./RevisionsItem"; +import { ScaleItem } from "./scaling/ScaleItem"; + +const refreshContainerAppEmitter = new EventEmitter(); +const refreshContainerAppEvent = refreshContainerAppEmitter.event; + +export function refreshContainerApp(id: string): void { + refreshContainerAppEmitter.fire(id); +} + +export interface ContainerAppModel extends ContainerApp { + id: string; + name: string; + resourceGroup: string; + managedEnvironmentId: string; + revisionsMode: KnownActiveRevisionsMode; +} + +export class ContainerAppItem implements ContainerAppsItem, IDeletable { + public static contextValue: string = 'containerApp'; + public static contextValueRegExp: RegExp = new RegExp(ContainerAppItem.contextValue); + + id: string; + + private resourceGroup: string; + private name: string; + + + public get containerApp(): ContainerAppModel { + return this._containerApp; + } + + constructor(public readonly subscription: AzureSubscription, private _containerApp: ContainerAppModel) { + this.id = this.containerApp.id; + this.resourceGroup = this.containerApp.resourceGroup; + this.name = this.containerApp.name; + refreshContainerAppEvent((id) => { + if (id === this.id) { + void this.refresh(); + } + }) + } + + private async refresh(): Promise { + await callWithTelemetryAndErrorHandling('containerAppItem.refresh', async (context) => { + const client: ContainerAppsAPIClient = await createContainerAppsClient(context, this.subscription); + this._containerApp = ContainerAppItem.CreateContainerAppModel(await client.containerApps.get(this.resourceGroup, this.name)); + ext.branchDataProvider.refresh(this); + }); + } + + viewProperties: ViewPropertiesModel = { + data: this.containerApp, + label: this.containerApp.name, + } + + portalUrl: Uri = createPortalUrl(this.subscription, this.containerApp.id); + + async getChildren(): Promise { + + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context) => { + const children: TreeElementBase[] = []; + children.push(this.containerApp.configuration?.dapr?.enabled ? new DaprEnabledItem(this.containerApp, this.containerApp.configuration.dapr) : createDaprDisabledItem(this.containerApp)); + + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, createSubscriptionContext(this.subscription)]); + const revisionData = await client.containerAppsRevisions.getRevision(this.resourceGroup, this.name, nonNullProp(this.containerApp, 'latestRevisionName')); + + if (this.containerApp.revisionsMode === KnownActiveRevisionsMode.Multiple) { + children.push(new RevisionsItem(this.subscription, this.containerApp)); + } else { + children.push(new ScaleItem(this.subscription, this.containerApp, revisionData)); + } + + children.push(this.containerApp.configuration?.ingress ? new IngressItem(this.subscription, this.containerApp) : new IngressDisabledItem(this.subscription, this.containerApp)); + children.push(new LogsItem(this.subscription, this.containerApp)); + + return children; + }); + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + label: nonNullProp(this.containerApp, 'name'), + iconPath: treeUtils.getIconPath('azure-containerapps'), + contextValue: `containerApp|revisionmode:${this.containerApp.revisionsMode}`, + description: this.containerApp.provisioningState === 'Succeeded' ? undefined : this.containerApp.provisioningState, + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + static async List(context: IActionContext, subscription: AzureSubscription, managedEnvironmentId: string): Promise { + const subContext = createSubscriptionContext(subscription); + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, subContext]); + return (await uiUtils.listAllIterator(client.containerApps.listBySubscription())) + .filter(ca => ca.managedEnvironmentId && ca.managedEnvironmentId === managedEnvironmentId) + .map(ContainerAppItem.CreateContainerAppModel); + } + + static async Get(context: IActionContext, subscription: AzureSubscription, resourceGroupName: string, containerAppName: string): Promise { + const client: ContainerAppsAPIClient = await createContainerAppsClient(context, subscription); + return ContainerAppItem.CreateContainerAppModel(await client.containerApps.get(resourceGroupName, containerAppName)); + } + + static CreateContainerAppModel(containerApp: ContainerApp): ContainerAppModel { + const revisionsMode = containerApp.configuration?.activeRevisionsMode as KnownActiveRevisionsMode ?? KnownActiveRevisionsMode.Single; + return { + id: nonNullProp(containerApp, 'id'), + name: nonNullProp(containerApp, 'name'), + managedEnvironmentId: nonNullProp(containerApp, 'managedEnvironmentId'), + resourceGroup: getResourceGroupFromId(nonNullProp(containerApp, 'id')), + revisionsMode, + ...containerApp, + } + } + + async delete(context: IActionContext & { suppressPrompt?: boolean }): Promise { + const confirmMessage: string = localize('confirmDeleteContainerApp', 'Are you sure you want to delete container app "{0}"?', this.name); + const deleteContainerApp: string = localize('deleteContainerApp', 'Delete Container App "{0}"', this.name); + + const wizardContext: IDeleteContainerAppWizardContext = { + activityTitle: deleteContainerApp, + containerAppNames: this.name, + subscription: createSubscriptionContext(this.subscription), + resourceGroupName: this.resourceGroup, + ...context, + ...(await createActivityContext()) + }; + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + promptSteps: [new DeleteConfirmationStep(confirmMessage)], + executeSteps: [new DeleteAllContainerAppsStep()] + }); + + if (!context.suppressPrompt) { + await wizard.prompt(); + } + + await ext.state.runWithTemporaryDescription(this.containerApp.id, localize('deleting', 'Deleting...'), async () => { + await wizard.execute(); + }); + ext.state.notifyChildrenChanged(this.containerApp.managedEnvironmentId); + } +} + +export async function getContainerEnvelopeWithSecrets(context: IActionContext, subscription: AzureSubscription, containerApp: ContainerAppModel): Promise> { + // anytime you want to update the container app, you need to include the secrets but that is not retrieved by default + // make a deep copy, we don't want to modify the one that is cached + const containerAppEnvelope = JSON.parse(JSON.stringify(containerApp)); + + // verify all top-level properties + for (const key of Object.keys(containerAppEnvelope)) { + containerAppEnvelope[key] = nonNullProp(containerAppEnvelope, key); + } + + const concreteContainerAppEnvelope = >containerAppEnvelope; + const webClient: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, createSubscriptionContext(subscription)]); + + concreteContainerAppEnvelope.configuration.secrets = ((await webClient.containerApps.listSecrets(containerApp.resourceGroup, containerApp.name)).value); + concreteContainerAppEnvelope.configuration.registries ||= []; + + return concreteContainerAppEnvelope; +} + +export function isIngressEnabled(containerApp: ContainerApp): containerApp is IngressEnabledContainerApp { + return !!containerApp.configuration?.ingress?.fqdn; +} + +type IngressEnabledContainerApp = ContainerApp & { configuration: { ingress: { fqdn: string } } }; diff --git a/src/tree/ContainerAppTreeItem.ts b/src/tree/ContainerAppTreeItem.ts deleted file mode 100644 index 7880f89e0..000000000 --- a/src/tree/ContainerAppTreeItem.ts +++ /dev/null @@ -1,202 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ContainerApp, ContainerAppsAPIClient } from "@azure/arm-appcontainers"; -import { getResourceGroupFromId } from "@microsoft/vscode-azext-azureutils"; -import { AzExtParentTreeItem, AzExtTreeItem, AzureWizard, DeleteConfirmationStep, IActionContext, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { DeleteAllContainerAppsStep } from "../commands/deleteContainerApp/DeleteAllContainerAppsStep"; -import { IDeleteContainerAppWizardContext } from "../commands/deleteContainerApp/IDeleteContainerAppWizardContext"; -import { azResourceContextValue, RevisionConstants } from "../constants"; -import { createActivityContext } from "../utils/activityUtils"; -import { createContainerAppsAPIClient } from "../utils/azureClients"; -import { localize } from "../utils/localize"; -import { nonNullProp } from "../utils/nonNull"; -import { openUrl } from "../utils/openUrl"; -import { treeUtils } from "../utils/treeUtils"; -import { DaprDisabledTreeItem, DaprEnabledTreeItem } from "./DaprTreeItem"; -import { IAzureResourceTreeItem } from './IAzureResourceTreeItem'; -import { IngressDisabledTreeItem, IngressTreeItem } from "./IngressTreeItem"; -import { LogsTreeItem } from "./LogsTreeItem"; -import { RevisionsTreeItem } from "./RevisionsTreeItem"; -import { RevisionTreeItem } from "./RevisionTreeItem"; -import { ScaleTreeItem } from "./ScaleTreeItem"; - -export class ContainerAppTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'containerApp'; - public static contextValueRegExp: RegExp = new RegExp(ContainerAppTreeItem.contextValue); - public contextValue: string; - public data: ContainerApp; - public resourceGroupName: string; - - public name: string; - public label: string; - public childTypeLabel: string = localize('containerAppSetting', 'Container App setting'); - public managedEnvironmentId: string; - - public revisionsTreeItem: RevisionsTreeItem; - public ingressTreeItem: IngressTreeItem | IngressDisabledTreeItem; - public logTreeItem: LogsTreeItem; - public scaleTreeItem: ScaleTreeItem; - - constructor(parent: AzExtParentTreeItem, ca: ContainerApp) { - super(parent); - this.data = ca; - - this.id = nonNullProp(this.data, 'id'); - this.resourceGroupName = getResourceGroupFromId(this.id); - - this.name = nonNullProp(this.data, 'name'); - this.label = this.name; - this.managedEnvironmentId = nonNullProp(this.data, 'managedEnvironmentId'); - - this.contextValue = `${ContainerAppTreeItem.contextValue}|${azResourceContextValue}|revisionmode:${this.getRevisionMode()}`; - } - - public get iconPath(): TreeItemIconPath { - return treeUtils.getIconPath('azure-containerapps'); - } - - public get description(): string | undefined { - return this.data.provisioningState === 'Succeeded' ? undefined : this.data.provisioningState; - } - - public async loadMoreChildrenImpl(_clearCache: boolean, context: IActionContext): Promise { - const children: AzExtTreeItem[] = []; - await this.updateChildren(context); - - if (this.data.configuration?.dapr?.enabled) { - children.push(new DaprEnabledTreeItem(this, this.data.configuration?.dapr)); - } else { - children.push(new DaprDisabledTreeItem(this)); - } - - if (this.getRevisionMode() === RevisionConstants.multiple.data) { - children.push(this.revisionsTreeItem); - } else { - children.push(this.scaleTreeItem); - } - - children.push(this.ingressTreeItem, this.logTreeItem) - return children; - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public compareChildrenImpl(): number { - return 0; - } - - public async browse(): Promise { - // make sure that ingress is enabled - if (!this.ingressEnabled() || !this.data.configuration?.ingress?.fqdn) { - throw new Error(localize('enableIngress', 'Enable ingress to perform this action.')); - } - - await openUrl(`https://${this.data.configuration?.ingress?.fqdn}`); - } - - public async deleteTreeItemImpl(context: IActionContext & { suppressPrompt?: boolean }): Promise { - const confirmMessage: string = localize('confirmDeleteContainerApp', 'Are you sure you want to delete container app "{0}"?', this.name); - const deleteContainerApp: string = localize('deleteContainerApp', 'Delete Container App "{0}"', this.name); - - const wizardContext: IDeleteContainerAppWizardContext = { - activityTitle: deleteContainerApp, - containerAppNames: this.name, - subscription: this.subscription, - resourceGroupName: this.resourceGroupName, - ...context, - ...(await createActivityContext()) - }; - const wizard: AzureWizard = new AzureWizard(wizardContext, { - promptSteps: [new DeleteConfirmationStep(confirmMessage)], - executeSteps: [new DeleteAllContainerAppsStep()] - }); - - if (!context.suppressPrompt) { - await wizard.prompt(); - } - await wizard.execute(); - } - - public async refreshImpl(context: IActionContext): Promise { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this]); - const data = await client.containerApps.get(this.resourceGroupName, this.name); - - this.contextValue = `${ContainerAppTreeItem.contextValue}|${azResourceContextValue}|revisionmode:${this.getRevisionMode()}`; - this.data = data; - - await this.updateChildren(context); - } - - public async updateChildren(context: IActionContext): Promise { - if (this.getRevisionMode() === RevisionConstants.multiple.data) { - this.revisionsTreeItem = new RevisionsTreeItem(this); - } - - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this]); - const revisionData = await client.containerAppsRevisions.getRevision(this.resourceGroupName, this.name, nonNullProp(this.data, 'latestRevisionName')); - this.scaleTreeItem = new ScaleTreeItem(this, revisionData.template?.scale); - - this.ingressTreeItem = this.data.configuration?.ingress ? new IngressTreeItem(this, this.data.configuration?.ingress) : new IngressDisabledTreeItem(this); - this.logTreeItem = new LogsTreeItem(this); - } - - public pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): AzExtTreeItem | undefined { - for (const expectedContextValue of expectedContextValues) { - if (expectedContextValue instanceof RegExp) { - if (expectedContextValue.test(ScaleTreeItem.contextValue)) { - return this.getRevisionMode() === RevisionConstants.single.data ? this.scaleTreeItem : this.revisionsTreeItem; - } - } else { - switch (expectedContextValue) { - case RevisionTreeItem.contextValue: - case RevisionsTreeItem.contextValue: - return this.revisionsTreeItem; - case IngressTreeItem.contextValue: - case IngressDisabledTreeItem.contextValue: - return this.ingressTreeItem; - case LogsTreeItem.contextValue: - return this.logTreeItem; - case ScaleTreeItem.contextValue: - return this.scaleTreeItem; - default: - } - } - } - - return undefined; - } - - public async getContainerEnvelopeWithSecrets(context: IActionContext): Promise> { - // anytime you want to update the container app, you need to include the secrets but that is not retrieved by default - // make a deep copy, we don't want to modify the one that is cached - const containerAppEnvelope = JSON.parse(JSON.stringify(this.data)); - - // verify all top-level properties - for (const key of Object.keys(containerAppEnvelope)) { - containerAppEnvelope[key] = nonNullProp(containerAppEnvelope, key); - } - - const concreteContainerAppEnvelope = >containerAppEnvelope; - const webClient: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this]); - - concreteContainerAppEnvelope.configuration.secrets = ((await webClient.containerApps.listSecrets(this.resourceGroupName, this.name)).value); - concreteContainerAppEnvelope.configuration.registries ||= []; - - return concreteContainerAppEnvelope; - } - - public getRevisionMode(): string { - return this.data.configuration?.activeRevisionsMode?.toLowerCase() === 'single' ? - RevisionConstants.single.data : RevisionConstants.multiple.data; - } - - public ingressEnabled(): boolean { - return !!this.data.configuration?.ingress; - } -} - diff --git a/src/tree/ContainerAppsBranchDataProvider.ts b/src/tree/ContainerAppsBranchDataProvider.ts new file mode 100644 index 000000000..e07a5e21b --- /dev/null +++ b/src/tree/ContainerAppsBranchDataProvider.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { AzExtServiceClientCredentials, callWithTelemetryAndErrorHandling, IActionContext, ISubscriptionContext, nonNullProp } from '@microsoft/vscode-azext-utils'; +import { AzureResource, AzureResourceBranchDataProvider, AzureSubscription, ResourceModelBase, ViewPropertiesModel } from '@microsoft/vscode-azext-utils/hostapi.v2'; +import * as vscode from 'vscode'; +import { ext } from '../extensionVariables'; +import { localize } from '../utils/localize'; +import { ContainerAppModel } from './ContainerAppItem'; +import { ManagedEnvironmentItem } from './ManagedEnvironmentItem'; + +export interface TreeElementBase extends ResourceModelBase { + getChildren?(): vscode.ProviderResult; + getTreeItem(): vscode.TreeItem | Thenable; + + viewProperties?: ViewPropertiesModel; +} + +export interface ContainerAppsItem extends TreeElementBase { + subscription: AzureSubscription; + containerApp: ContainerAppModel; +} + +export class ContainerAppsBranchDataProvider extends vscode.Disposable implements AzureResourceBranchDataProvider { + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + + constructor() { + super( + () => { + this.onDidChangeTreeDataEmitter.dispose(); + }); + } + + get onDidChangeTreeData(): vscode.Event { + return this.onDidChangeTreeDataEmitter.event; + } + + async getChildren(element: TreeElementBase): Promise { + return (await element.getChildren?.())?.map((child) => { + if (child.id) { + return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () => this.refresh(child)) + } + return child; + }); + } + + async getResourceItem(element: AzureResource): Promise { + const resourceItem = await callWithTelemetryAndErrorHandling( + 'getResourceItem', + async (context: IActionContext) => { + const managedEnvironment = await ManagedEnvironmentItem.Get(context, element.subscription, nonNullProp(element, 'resourceGroup'), element.name); + return new ManagedEnvironmentItem(element.subscription, element, managedEnvironment); + }); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return ext.state.wrapItemInStateHandling(resourceItem!, () => this.refresh(resourceItem)); + } + + async getTreeItem(element: TreeElementBase): Promise { + const ti = await element.getTreeItem(); + return ti; + } + + refresh(element?: TreeElementBase): void { + this.onDidChangeTreeDataEmitter.fire(element); + } +} + +export const branchDataProvider = new ContainerAppsBranchDataProvider(); + + +/** + * Converts a VS Code authentication session to an Azure Track 1 & 2 compatible compatible credential. + */ +export function createCredential(getSession: (scopes?: string[]) => vscode.ProviderResult): AzExtServiceClientCredentials { + return { + getToken: async (scopes?: string | string[]) => { + if (typeof scopes === 'string') { + scopes = [scopes]; + } + + const session = await getSession(scopes); + + if (session) { + return { + token: session.accessToken + }; + } else { + return null; + } + }, + signRequest: async () => { + throw new Error((localize('signRequestError', 'Track 1 credentials are not (currently) supported.'))); + } + }; +} + +/** + * Creates a subscription context from an application subscription. + */ +export function createSubscriptionContext(subscription: AzureSubscription): ISubscriptionContext { + return { + subscriptionDisplayName: '', + subscriptionPath: '', + userId: '', + ...subscription, + credentials: createCredential(subscription.authentication.getSession) + }; +} diff --git a/src/tree/DaprItem.ts b/src/tree/DaprItem.ts new file mode 100644 index 000000000..aeb5c2e70 --- /dev/null +++ b/src/tree/DaprItem.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { ContainerApp, Dapr } from "@azure/arm-appcontainers"; +import { ViewPropertiesModel } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { createGenericItem } from "../utils/GenericItem"; +import { localize } from "../utils/localize"; +import { treeUtils } from "../utils/treeUtils"; +import { ContainerAppModel } from "./ContainerAppItem"; +import { TreeElementBase } from "./ContainerAppsBranchDataProvider"; + +export class DaprEnabledItem implements TreeElementBase { + + id: string = `${this.containerApp.id}/DaprEnabled`; + constructor(private readonly containerApp: ContainerAppModel, private readonly dapr: Dapr) { } + + viewProperties: ViewPropertiesModel = { + data: this.dapr, + label: localize('daprProperties', '{0} Dapr', this.containerApp.name), + } + + getTreeItem(): TreeItem { + return { + id: this.id, + label: localize('dapr', 'Dapr'), + description: localize('enabled', 'Enabled'), + iconPath: treeUtils.getIconPath('dapr_logo'), + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + async getChildren(): Promise { + const children: TreeElementBase[] = []; + + if (this.dapr.appId) { + children.push(createGenericItem({ + contextValue: 'daprAppId', + description: 'app id', + iconPath: new ThemeIcon('dash'), + label: this.dapr.appId, + })); + } + + if (this.dapr.appPort) { + children.push(createGenericItem({ + contextValue: 'daprAppPort', + description: 'app port', + iconPath: new ThemeIcon('dash'), + label: String(this.dapr.appPort), + })) + } + + if (this.dapr.appProtocol) { + children.push(createGenericItem({ + description: 'app protocol', + label: String(this.dapr.appProtocol), + contextValue: 'daprAppProtocol', + iconPath: new ThemeIcon('dash'), + })); + } + + return children; + } +} + +export function createDaprDisabledItem(containerApp: ContainerApp): TreeElementBase { + return createGenericItem({ + id: `${containerApp.id}/DaprDisabled`, + label: localize('dapr', 'Dapr'), + description: localize('disabled', 'Disabled'), + contextValue: 'dapr|disabled', + iconPath: new ThemeIcon('debug-disconnect'), + }); +} diff --git a/src/tree/DaprTreeItem.ts b/src/tree/DaprTreeItem.ts deleted file mode 100644 index b0dc939f7..000000000 --- a/src/tree/DaprTreeItem.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dapr } from "@azure/arm-appcontainers"; -import { AzExtParentTreeItem, AzExtTreeItem, GenericTreeItem, IActionContext, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { azResourceContextValue } from "../constants"; -import { localize } from "../utils/localize"; -import { treeUtils } from "../utils/treeUtils"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; -import { IAzureResourceTreeItem } from "./IAzureResourceTreeItem"; - -export class DaprEnabledTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'dapr|enabled'; - public readonly contextValue: string = `${DaprEnabledTreeItem.contextValue}|${azResourceContextValue}`; - public readonly parent: ContainerAppTreeItem; - public data: Dapr; - - public label: string; - - constructor(parent: ContainerAppTreeItem, data: Dapr | undefined) { - super(parent); - this.label = localize('dapr', 'Dapr'); - this.data = data || {}; - this.description = localize('enabled', 'Enabled'); - } - - public get iconPath(): TreeItemIconPath { - return treeUtils.getIconPath('dapr_logo'); - } - - public async loadMoreChildrenImpl(_clearCache: boolean, _context: IActionContext): Promise { - const children: AzExtTreeItem[] = []; - this.data.appId ? children.push(new GenericTreeItem(this, { description: 'app id', label: this.data.appId, contextValue: 'daprAppId', iconPath: new ThemeIcon('dash') })) : undefined; - this.data.appPort ? children.push(new GenericTreeItem(this, { description: 'app port', label: String(this.data.appPort), contextValue: 'daprAppPort', iconPath: new ThemeIcon('dash') })) : undefined; - this.data.appProtocol ? children.push(new GenericTreeItem(this, { description: 'app protocol', label: String(this.data.appProtocol), contextValue: 'daprAppProtocol', iconPath: new ThemeIcon('dash') })) : undefined; - - return children; - } - - public hasMoreChildrenImpl(): boolean { - return false; - } -} - -export class DaprDisabledTreeItem extends AzExtTreeItem { - public static contextValue: string = 'dapr|disabled'; - public readonly contextValue: string = DaprDisabledTreeItem.contextValue; - public readonly parent: ContainerAppTreeItem; - - public label: string; - - constructor(parent: ContainerAppTreeItem) { - super(parent); - this.label = localize('dapr', 'Dapr'); - this.description = localize('disabled', 'Disabled'); - } - - public get iconPath(): TreeItemIconPath { - return new ThemeIcon('debug-disconnect'); - } -} diff --git a/src/tree/IAzureResourceTreeItem.ts b/src/tree/IAzureResourceTreeItem.ts deleted file mode 100644 index cb082c3ba..000000000 --- a/src/tree/IAzureResourceTreeItem.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem } from "@microsoft/vscode-azext-utils"; - -export interface IAzureResource { - data: {} | undefined; - - /** - * Implement this to execute any async code when data is undefined - */ - getDataImpl?(): Promise; -} - -export interface IAzureResourceTreeItem extends IAzureResource, AzExtTreeItem { } diff --git a/src/tree/IngressItem.ts b/src/tree/IngressItem.ts new file mode 100644 index 000000000..cf893b6be --- /dev/null +++ b/src/tree/IngressItem.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ContainerApp, Ingress } from "@azure/arm-appcontainers"; +import { AzureSubscription, ViewPropertiesModel } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { azResourceContextValue, IngressConstants } from "../constants"; +import { createGenericItem } from "../utils/GenericItem"; +import { localize } from "../utils/localize"; +import { treeUtils } from "../utils/treeUtils"; +import { ContainerAppModel } from "./ContainerAppItem"; +import { ContainerAppsItem, TreeElementBase } from "./ContainerAppsBranchDataProvider"; + +const label: string = localize('ingress', 'Ingress'); + +export class IngressItem implements ContainerAppsItem { + static contextValue: string = 'ingress|enabled'; + readonly contextValue: string = `${IngressItem.contextValue}|${azResourceContextValue}`; + + constructor(readonly subscription: AzureSubscription, readonly containerApp: ContainerAppModel) { } + + id: string = `${this.containerApp.id}/ingress`; + + ingress: Ingress = this.containerApp.configuration?.ingress ?? {}; + + viewProperties: ViewPropertiesModel = { + data: this.ingress, + label: `${this.containerApp.name} ${label}`, + } + + getTreeItem(): TreeItem { + return { + label, + contextValue: IngressItem.contextValue, + iconPath: treeUtils.getIconPath('10061-icon-Virtual Networks-Networking'), + collapsibleState: TreeItemCollapsibleState.Collapsed, + }; + } + + async getChildren(): Promise { + const label: string = this.ingress.external ? IngressConstants.external : IngressConstants.internal; + const description: string = this.ingress.external ? IngressConstants.externalDesc : IngressConstants.internalDesc; + + return [ + createGenericItem({ + contextValue: 'visibility', + description, + iconPath: new ThemeIcon('dash'), + label, + }), + createGenericItem({ + contextValue: 'targetPort', + description: String(this.ingress.targetPort), + iconPath: new ThemeIcon('dash'), + label: localize('targetPort', 'Target Port'), + }), + ]; + } +} + +export class IngressDisabledItem implements TreeElementBase { + public static contextValue: string = 'ingress|disabled'; + public readonly contextValue: string = IngressDisabledItem.contextValue; + + constructor(public readonly subscription: AzureSubscription, public readonly containerApp: ContainerApp) { } + + getTreeItem(): TreeItem { + return { + label, + description: localize('disabled', 'Disabled'), + contextValue: IngressDisabledItem.contextValue, + iconPath: new ThemeIcon('debug-disconnect'), + } + } +} + +export function createTargetPortItem(subscription: AzureSubscription, containerApp: ContainerAppModel): ContainerAppsItem { + return { + subscription, + containerApp, + ...createGenericItem({ + label: localize('targetPort', 'Target Port'), + contextValue: 'targetPort', + description: String(containerApp.configuration?.ingress?.targetPort), + iconPath: new ThemeIcon('dash'), + }), + }; +} diff --git a/src/tree/IngressTreeItem.ts b/src/tree/IngressTreeItem.ts deleted file mode 100644 index cba176ea1..000000000 --- a/src/tree/IngressTreeItem.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Ingress } from "@azure/arm-appcontainers"; -import { AzExtParentTreeItem, AzExtTreeItem, GenericTreeItem, IActionContext, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { azResourceContextValue, IngressConstants } from "../constants"; -import { localize } from "../utils/localize"; -import { treeUtils } from "../utils/treeUtils"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; -import { IAzureResourceTreeItem } from "./IAzureResourceTreeItem"; - -const label: string = localize('ingress', 'Ingress'); - -export class IngressTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'ingress|enabled'; - public readonly contextValue: string = `${IngressTreeItem.contextValue}|${azResourceContextValue}`; - public readonly parent: ContainerAppTreeItem; - public data: Ingress - - public label: string; - - constructor(parent: ContainerAppTreeItem, data?: Ingress) { - super(parent); - this.data = data || {}; - this.label = label; - } - - public async loadMoreChildrenImpl(_clearCache: boolean, _context: IActionContext): Promise { - const label: string = this.data.external ? IngressConstants.external : IngressConstants.internal; - const description: string = this.data.external ? IngressConstants.externalDesc : IngressConstants.internalDesc; - - return [ - new GenericTreeItem(this, { label: localize('targetPort', 'Target Port'), contextValue: 'targetPort', description: String(this.data.targetPort), iconPath: new ThemeIcon('dash') }), - new GenericTreeItem(this, { label, contextValue: 'visibility', description, iconPath: new ThemeIcon('dash') }) - ]; - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public get iconPath(): TreeItemIconPath { - return treeUtils.getIconPath('10061-icon-Virtual Networks-Networking'); - } -} - -export class IngressDisabledTreeItem extends AzExtTreeItem { - public static contextValue: string = 'ingress|disabled'; - public readonly contextValue: string = IngressDisabledTreeItem.contextValue; - public readonly parent: ContainerAppTreeItem; - - public label: string; - - constructor(parent: ContainerAppTreeItem) { - super(parent); - this.label = label; - this.description = localize('disabled', 'Disabled'); - } - - public get iconPath(): TreeItemIconPath { - return new ThemeIcon('debug-disconnect'); - } -} diff --git a/src/tree/LogsItem.ts b/src/tree/LogsItem.ts new file mode 100644 index 000000000..47cef21e1 --- /dev/null +++ b/src/tree/LogsItem.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { ContainerApp } from "@azure/arm-appcontainers"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { createPortalUrl } from "../utils/createPortalUrl"; +import { createGenericItem } from "../utils/GenericItem"; +import { localize } from "../utils/localize"; +import { TreeElementBase } from "./ContainerAppsBranchDataProvider"; + +export class LogsItem implements TreeElementBase { + constructor(private readonly subscription: AzureSubscription, private readonly containerApp: ContainerApp) { } + id: string = `${this.containerApp.id}/Logs`; + + getTreeItem(): TreeItem { + return { + collapsibleState: TreeItemCollapsibleState.Collapsed, + contextValue: 'logs', + iconPath: new ThemeIcon('book'), + id: this.id, + label: localize('logs', 'Logs'), + }; + } + + async getChildren(): Promise { + const iconPath = new ThemeIcon('link-external'); + const openInPortal = 'azureResourceGroups.openInPortal'; + return [ + createGenericItem({ + contextValue: 'openLogs', + commandId: openInPortal, + iconPath, + id: `${this.containerApp.id}/logs`, + label: localize('openLogs', 'Open Logs'), + commandArgs: [{ + portalUrl: createPortalUrl(this.subscription, `${this.containerApp.id}/logs`), + }] + }), + createGenericItem({ + contextValue: 'openLogStream', + commandId: openInPortal, + iconPath, + id: `${this.id}/logstream`, + label: localize('openLogStream', 'Open Log Stream'), + commandArgs: [{ + portalUrl: createPortalUrl(this.subscription, `${this.containerApp.id}/logstream`), + }] + }), + ]; + } +} diff --git a/src/tree/LogsTreeItem.ts b/src/tree/LogsTreeItem.ts deleted file mode 100644 index d145e5d06..000000000 --- a/src/tree/LogsTreeItem.ts +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, GenericTreeItem, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { localize } from "../utils/localize"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; - -export class LogsTreeItem extends AzExtParentTreeItem { - public static contextValue: string = 'log'; - public static openLogsContext: string = 'openLog'; - public readonly contextValue: string = LogsTreeItem.contextValue; - public readonly parent: ContainerAppTreeItem; - - public label: string; - public childTypeLabel: string = localize('logView', 'log view'); - - constructor(parent: ContainerAppTreeItem) { - super(parent); - this.id = `${this.parent.id}/logParent`; - this.label = localize('logs', 'Logs'); - } - - public get iconPath(): TreeItemIconPath { - return new ThemeIcon('book'); - } - - public async loadMoreChildrenImpl(): Promise { - const iconPath = new ThemeIcon('link-external'); - return [ - new GenericTreeItem(this, { label: 'Open Logs', contextValue: 'openLogs', commandId: 'containerApps.openInPortal', iconPath, id: `${this.parent.id}/logs` }), - new GenericTreeItem(this, { label: 'Open Log Stream', contextValue: 'openLogStream', commandId: 'containerApps.openInPortal', iconPath, id: `${this.parent.id}/logstream` }) - ] - } - - public hasMoreChildrenImpl(): boolean { - return false; - } -} diff --git a/src/tree/ManagedEnvironmentItem.ts b/src/tree/ManagedEnvironmentItem.ts new file mode 100644 index 000000000..9990627c4 --- /dev/null +++ b/src/tree/ManagedEnvironmentItem.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { ContainerAppsAPIClient, ManagedEnvironment, Resource } from "@azure/arm-appcontainers"; +import { getResourceGroupFromId, uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { callWithTelemetryAndErrorHandling, IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; +import { AzureResource, AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { TreeItem, TreeItemCollapsibleState } from "vscode"; +import { createContainerAppsAPIClient } from "../utils/azureClients"; +import { treeUtils } from "../utils/treeUtils"; +import { ContainerAppItem } from "./ContainerAppItem"; +import { ContainerAppsItem, createSubscriptionContext, TreeElementBase } from "./ContainerAppsBranchDataProvider"; + +type ManagedEnvironmentModel = ManagedEnvironment & ResourceModel; + +export class ManagedEnvironmentItem implements TreeElementBase { + + public static contextValue: string = 'containerEnvironment'; + id: string; + + constructor(public readonly subscription: AzureSubscription, public readonly resource: AzureResource, public readonly managedEnvironment: ManagedEnvironmentModel) { + this.id = managedEnvironment.id; + } + + async getChildren(): Promise { + + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context) => { + const containerApps = await ContainerAppItem.List(context, this.subscription, this.id); + return containerApps.map(ca => new ContainerAppItem(this.subscription, ca)); + }); + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + label: this.managedEnvironment.name, + id: this.id, + iconPath: treeUtils.getIconPath('managedEnvironment'), + contextValue: ManagedEnvironmentItem.contextValue, + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + static async List(context: IActionContext, subscription: AzureSubscription): Promise { + const subContext = createSubscriptionContext(subscription); + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, subContext]); + return await uiUtils.listAllIterator(client.managedEnvironments.listBySubscription()); + } + + static async Get(context: IActionContext, subscription: AzureSubscription, resourceGroup: string, name: string): Promise { + const subContext = createSubscriptionContext(subscription); + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, subContext]); + return ManagedEnvironmentItem.CreateManagedEnvironmentModel(await client.managedEnvironments.get(resourceGroup, name)); + } + + private static CreateManagedEnvironmentModel(managedEnvironment: ManagedEnvironment): ManagedEnvironmentModel { + return createAzureResourceModel(managedEnvironment); + } +} + +interface ResourceModel extends Resource { + id: string; + name: string; + resourceGroup: string; +} + +function createAzureResourceModel(resource: T): T & ResourceModel { + const id = nonNullProp(resource, 'id'); + return { + id, + name: nonNullProp(resource, 'name'), + resourceGroup: getResourceGroupFromId(id), + ...resource, + } +} diff --git a/src/tree/ManagedEnvironmentTreeItem.ts b/src/tree/ManagedEnvironmentTreeItem.ts deleted file mode 100644 index db1110f78..000000000 --- a/src/tree/ManagedEnvironmentTreeItem.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ManagedEnvironment } from "@azure/arm-appcontainers"; -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, ICreateChildImplContext } from "@microsoft/vscode-azext-utils"; -import { IAzureResourceTreeItem } from './IAzureResourceTreeItem'; -import { ResolvedContainerEnvironmentResource } from "./ResolvedContainerAppsResource"; - -export class ManagedEnvironmentTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = ResolvedContainerEnvironmentResource.contextValue; - public static contextValueRegExp: RegExp = ResolvedContainerEnvironmentResource.contextValueRegExp; - public readonly contextValue: string; - public readonly data: ManagedEnvironment; - public readonly childTypeLabel: string; - public resourceGroupName: string; - - public resolved: ResolvedContainerEnvironmentResource; - - public name: string; - public label: string; - - constructor(parent: AzExtParentTreeItem, resolvedContainerAppsResource: ResolvedContainerEnvironmentResource) { - super(parent); - this.resolved = resolvedContainerAppsResource; - this.data = this.resolved.data; - - this.resourceGroupName = this.resolved.resourceGroupName; - this.name = this.resolved.name; - this.label = this.resolved.label; - this.contextValue = this.resolved.resolvedContextValue; - this.childTypeLabel = this.resolved.childTypeLabel; - } - - public get id(): string { - return this.resolved.id; - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - return await this.resolved.loadMoreChildrenImpl(clearCache, context); - } - - public hasMoreChildrenImpl(): boolean { - return this.resolved.hasMoreChildrenImpl(); - } - - public async createChildImpl(context: ICreateChildImplContext): Promise { - return this.resolved.createChildImpl(context); - } - - public async deleteTreeItemImpl(context: IActionContext & { suppressPrompt?: boolean }): Promise { - return await this.resolved.deleteTreeItemImpl(context); - } -} diff --git a/src/tree/ResolvedContainerAppsResource.ts b/src/tree/ResolvedContainerAppsResource.ts deleted file mode 100644 index f8c42f6d7..000000000 --- a/src/tree/ResolvedContainerAppsResource.ts +++ /dev/null @@ -1,135 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ContainerApp, ContainerAppsAPIClient, ManagedEnvironment } from "@azure/arm-appcontainers"; -import { getResourceGroupFromId, LocationListStep, uiUtils, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; -import { AzExtTreeItem, AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, ICreateChildImplContext, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; -import { ResolvedAppResourceBase } from "@microsoft/vscode-azext-utils/hostapi"; -import { ContainerAppCreateStep } from "../commands/createContainerApp/ContainerAppCreateStep"; -import { ContainerAppNameStep } from "../commands/createContainerApp/ContainerAppNameStep"; -import { EnableIngressStep } from "../commands/createContainerApp/EnableIngressStep"; -import { EnvironmentVariablesListStep } from "../commands/createContainerApp/EnvironmentVariablesListStep"; -import { IContainerAppWithActivityContext } from "../commands/createContainerApp/IContainerAppContext"; -import { DeleteAllContainerAppsStep } from "../commands/deleteContainerApp/DeleteAllContainerAppsStep"; -import { DeleteEnvironmentConfirmationStep } from "../commands/deleteManagedEnvironment/DeleteEnvironmentConfirmationStep"; -import { DeleteManagedEnvironmentStep } from "../commands/deleteManagedEnvironment/DeleteManagedEnvironmentStep"; -import { IDeleteManagedEnvironmentWizardContext } from "../commands/deleteManagedEnvironment/IDeleteManagedEnvironmentWizardContext"; -import { ContainerRegistryListStep } from "../commands/deployImage/ContainerRegistryListStep"; -import { azResourceContextValue, webProvider } from "../constants"; -import { createActivityContext } from "../utils/activityUtils"; -import { createContainerAppsAPIClient } from "../utils/azureClients"; -import { localize } from "../utils/localize"; -import { nonNullProp } from "../utils/nonNull"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; -import { IAzureResource } from "./IAzureResourceTreeItem"; -import { ManagedEnvironmentTreeItem } from "./ManagedEnvironmentTreeItem"; - -export class ResolvedContainerEnvironmentResource implements ResolvedAppResourceBase, IAzureResource { - public static contextValue: string = 'containerEnvironment'; - public static contextValueRegExp: RegExp = new RegExp(ResolvedContainerEnvironmentResource.contextValue); - public resolvedContextValue: string = `${ResolvedContainerEnvironmentResource.contextValue}|${azResourceContextValue}`; - public contextValuesToAdd: string[] = []; - - public readonly data: ManagedEnvironment; - public resourceGroupName: string; - public readonly childTypeLabel: string = localize('containerApp', 'Container App'); - - public name: string; - public label: string; - - public constructor(private readonly _subscription: ISubscriptionContext, ke: ManagedEnvironment) { - this.data = ke; - - this.resourceGroupName = getResourceGroupFromId(this.id); - this.name = nonNullProp(this.data, 'name'); - this.label = this.name; - - this.contextValuesToAdd.push(this.resolvedContextValue); - } - - public get id(): string { - return nonNullProp(this.data, 'id'); - } - - public async loadMoreChildrenImpl(_clearCache: boolean, context: IActionContext): Promise { - const proxyTree: ManagedEnvironmentTreeItem = this as unknown as ManagedEnvironmentTreeItem; - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this._subscription]); - // could be more efficient to call this once at Subscription level, and filter based off that but then risk stale data - const containerApps: ContainerApp[] = (await uiUtils.listAllIterator(client.containerApps.listBySubscription())) - .filter(ca => ca.managedEnvironmentId && ca.managedEnvironmentId === this.id) - - return await proxyTree.createTreeItemsWithErrorHandling( - containerApps, - 'invalidContainerApp', - ca => new ContainerAppTreeItem(proxyTree, ca), - ca => ca.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public async createChildImpl(context: ICreateChildImplContext): Promise { - const proxyTree: ManagedEnvironmentTreeItem = this as unknown as ManagedEnvironmentTreeItem; - const wizardContext: IContainerAppWithActivityContext = { - ...context, ...this._subscription, managedEnvironmentId: this.id, ...(await createActivityContext()) - }; - - const title: string = localize('createContainerApp', 'Create Container App'); - const promptSteps: AzureWizardPromptStep[] = - [new ContainerAppNameStep(), new ContainerRegistryListStep(), new EnvironmentVariablesListStep(), new EnableIngressStep()]; - const executeSteps: AzureWizardExecuteStep[] = [new VerifyProvidersStep([webProvider]), new ContainerAppCreateStep()]; - - wizardContext.newResourceGroupName = this.resourceGroupName; - await LocationListStep.setLocation(wizardContext, this.data.location); - - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title, - promptSteps, - executeSteps, - showLoadingPrompt: true - }); - - await wizard.prompt(); - const newContainerAppName = nonNullProp(wizardContext, 'newContainerAppName'); - context.showCreatingTreeItem(newContainerAppName); - wizardContext.activityTitle = localize('createNamedContainerApp', 'Create Container App "{0}"', newContainerAppName); - try { - await wizard.execute(); - } finally { - // refresh this node even if create fails because container app provision failure throws an error, but still creates a container app - await proxyTree.refresh(context); - } - - return new ContainerAppTreeItem(proxyTree, nonNullProp(wizardContext, 'containerApp')); - } - - public async deleteTreeItemImpl(context: IActionContext & { suppressPrompt?: boolean }): Promise { - const proxyTree: ManagedEnvironmentTreeItem = this as unknown as ManagedEnvironmentTreeItem; - const containerApps = (await proxyTree.loadAllChildren(context)); - - const deleteManagedEnvironment: string = localize('deleteManagedEnvironment', 'Delete Container Apps environment "{0}"', proxyTree.name); - - const wizardContext: IDeleteManagedEnvironmentWizardContext = { - activityTitle: deleteManagedEnvironment, - containerAppNames: containerApps.map(ca => ca.name), - managedEnvironmentName: proxyTree.name, - resourceGroupName: proxyTree.resourceGroupName, - subscription: proxyTree.subscription, - ...context, - ...(await createActivityContext()) - }; - const wizard: AzureWizard = new AzureWizard(wizardContext, { - promptSteps: [new DeleteEnvironmentConfirmationStep()], - executeSteps: [new DeleteAllContainerAppsStep(), new DeleteManagedEnvironmentStep()] - }); - - if (!context.suppressPrompt) { - await wizard.prompt(); - } - await wizard.execute(); - } -} diff --git a/src/tree/RevisionItem.ts b/src/tree/RevisionItem.ts new file mode 100644 index 000000000..02cea131a --- /dev/null +++ b/src/tree/RevisionItem.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { KnownRevisionProvisioningState, Revision } from "@azure/arm-appcontainers"; +import { nonNullProp, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription, ViewPropertiesModel } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { localize } from "../utils/localize"; +import { ContainerAppModel } from "./ContainerAppItem"; +import { ContainerAppsItem, TreeElementBase } from "./ContainerAppsBranchDataProvider"; +import { ScaleItem } from "./scaling/ScaleItem"; + +export interface RevisionsItemModel extends ContainerAppsItem { + revision: Revision; +} + +export class RevisionItem implements RevisionsItemModel { + id: string; + + constructor(public readonly subscription: AzureSubscription, public readonly containerApp: ContainerAppModel, public readonly revision: Revision) { + this.id = nonNullProp(this.revision, 'id'); + } + + viewProperties: ViewPropertiesModel = { + data: this.revision, + label: nonNullProp(this.revision, 'name'), + } + + async getChildren(): Promise { + return [new ScaleItem(this.subscription, this.containerApp, this.revision)]; + } + + getTreeItem(): TreeItem { + const description = !this.revision.active ? + localize('inactive', 'Inactive') : + this.revision.name === this.containerApp.latestRevisionName ? + localize('latest', 'Latest') : + undefined; + + return { + id: this.id, + label: this.revision.name, + iconPath: this.iconPath, + description, + contextValue: 'revision', + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + private get iconPath(): TreeItemIconPath { + let id: string; + let colorId: string; + + if (!this.revision.active) { + id = 'circle-slash'; + colorId = 'testing.iconUnset'; + } else { + switch (this.revision.provisioningState) { + case KnownRevisionProvisioningState.Deprovisioning: + case KnownRevisionProvisioningState.Provisioning: + id = 'play-circle'; + colorId = 'testing.iconUnset'; + break; + case KnownRevisionProvisioningState.Failed: + id = 'error'; + colorId = 'testing.iconFailed'; + break; + case KnownRevisionProvisioningState.Provisioned: + id = 'pass' + colorId = 'testing.iconPassed'; + break; + case KnownRevisionProvisioningState.Deprovisioned: + default: + id = 'circle-slash'; + colorId = 'testing.iconUnset'; + } + } + + return new ThemeIcon(id, colorId ? new ThemeColor(colorId) : undefined); + } +} diff --git a/src/tree/RevisionTreeItem.ts b/src/tree/RevisionTreeItem.ts deleted file mode 100644 index 67485b1b8..000000000 --- a/src/tree/RevisionTreeItem.ts +++ /dev/null @@ -1,104 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { KnownRevisionProvisioningState, Revision } from "@azure/arm-appcontainers"; -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeColor, ThemeIcon } from "vscode"; -import { azResourceContextValue } from "../constants"; -import { localize } from "../utils/localize"; -import { nonNullProp } from "../utils/nonNull"; -import { IAzureResourceTreeItem } from './IAzureResourceTreeItem'; -import { RevisionsTreeItem } from "./RevisionsTreeItem"; -import { ScaleTreeItem } from "./ScaleTreeItem"; - -export class RevisionTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'revision'; - public readonly contextValue: string = `${RevisionTreeItem.contextValue}|${azResourceContextValue}`; - public data: Revision; - public readonly parent: RevisionsTreeItem; - - public scaleTreeItem: ScaleTreeItem; - - public name: string; - public label: string; - - constructor(parent: RevisionsTreeItem, re: Revision) { - super(parent); - this.data = re; - - this.id = nonNullProp(this.data, 'id'); - this.name = nonNullProp(this.data, 'name'); - this.label = this.name; - } - - public get description(): string | undefined { - return !this.data.active ? - localize('inactive', 'Inactive') : - this.name === this.parent.parent.data.latestRevisionName ? - localize('latest', 'Latest') : - undefined; - } - - public get iconPath(): TreeItemIconPath { - let id: string; - let colorId: string; - - if (!this.data.active) { - id = 'circle-slash'; - colorId = 'testing.iconUnset'; - } else { - switch (this.data.provisioningState) { - case KnownRevisionProvisioningState.Deprovisioning: - case KnownRevisionProvisioningState.Provisioning: - id = 'play-circle'; - colorId = 'testing.iconUnset'; - break; - case KnownRevisionProvisioningState.Failed: - id = 'error'; - colorId = 'testing.iconFailed'; - break; - case KnownRevisionProvisioningState.Provisioned: - id = 'pass' - colorId = 'testing.iconPassed'; - break; - case KnownRevisionProvisioningState.Deprovisioned: - default: - id = 'circle-slash'; - colorId = 'testing.iconUnset'; - } - } - - return new ThemeIcon(id, colorId ? new ThemeColor(colorId) : undefined); - } - - public async refreshImpl(context: IActionContext): Promise { - this.data = await this.parent.getRevision(context, this.name); - } - - public async loadMoreChildrenImpl(_clearCache: boolean, _context: IActionContext): Promise { - this.scaleTreeItem = new ScaleTreeItem(this, this.data.template?.scale); - return [this.scaleTreeItem]; - } - - public pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): AzExtTreeItem | undefined { - for (const expectedContextValue of expectedContextValues) { - if (expectedContextValue instanceof RegExp) { - if (expectedContextValue.test(ScaleTreeItem.contextValue)) return this.scaleTreeItem; - } else { - switch (expectedContextValue) { - case ScaleTreeItem.contextValue: - return this.scaleTreeItem; - default: - } - } - } - - return undefined; - } - - public hasMoreChildrenImpl(): boolean { - return false; - } -} diff --git a/src/tree/RevisionsItem.ts b/src/tree/RevisionsItem.ts new file mode 100644 index 000000000..376fddef7 --- /dev/null +++ b/src/tree/RevisionsItem.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { callWithTelemetryAndErrorHandling } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { TreeItem, TreeItemCollapsibleState } from "vscode"; +import { createContainerAppsAPIClient } from "../utils/azureClients"; +import { localize } from "../utils/localize"; +import { treeUtils } from "../utils/treeUtils"; +import { ContainerAppModel } from "./ContainerAppItem"; +import { ContainerAppsItem, createSubscriptionContext } from "./ContainerAppsBranchDataProvider"; +import { RevisionItem } from "./RevisionItem"; + +export class RevisionsItem implements ContainerAppsItem { + id: string; + + constructor(public readonly subscription: AzureSubscription, public readonly containerApp: ContainerAppModel) { + this.id = `${containerApp.id}/Revisions`; + } + + async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context) => { + const client = await createContainerAppsAPIClient([context, createSubscriptionContext(this.subscription)]); + const revisions = await uiUtils.listAllIterator(client.containerAppsRevisions.listRevisions(this.containerApp.resourceGroup, this.containerApp.name)); + return revisions.map(revision => new RevisionItem(this.subscription, this.containerApp, revision)); + }); + + return result?.reverse() ?? []; + } + + getTreeItem(): TreeItem { + return { + label: localize('revisions', 'Revisions'), + iconPath: treeUtils.getIconPath('02885-icon-menu-Container-Revision-Active'), + contextValue: 'revisions', + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } +} diff --git a/src/tree/RevisionsTreeItem.ts b/src/tree/RevisionsTreeItem.ts deleted file mode 100644 index 0e099ba85..000000000 --- a/src/tree/RevisionsTreeItem.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ContainerAppsAPIClient, Revision } from "@azure/arm-appcontainers"; -import { uiUtils } from "@microsoft/vscode-azext-azureutils"; -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { RevisionConstants } from "../constants"; -import { createContainerAppsAPIClient } from "../utils/azureClients"; -import { localize } from "../utils/localize"; -import { treeUtils } from "../utils/treeUtils"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; -import { RevisionTreeItem } from "./RevisionTreeItem"; - -export class RevisionsTreeItem extends AzExtParentTreeItem { - public static contextValue: string = 'revisions'; - public readonly contextValue: string = RevisionsTreeItem.contextValue; - public readonly childTypeLabel: string = localize('revision', 'Revision'); - public readonly parent: ContainerAppTreeItem; - - public name: string; - public label: string; - - constructor(parent: ContainerAppTreeItem) { - super(parent); - this.label = localize('revisons', 'Revisions'); - } - - public get iconPath(): TreeItemIconPath { - return treeUtils.getIconPath('02885-icon-menu-Container-Revision-Active'); - } - - public get description(): string { - return this.parent.data.configuration?.activeRevisionsMode?.toLowerCase() === 'single' ? RevisionConstants.single.label : RevisionConstants.multiple.label; - } - - public async loadMoreChildrenImpl(_clearCache: boolean, context: IActionContext): Promise { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this]); - const revisions: Revision[] = await uiUtils.listAllIterator(client.containerAppsRevisions.listRevisions(this.parent.resourceGroupName, this.parent.name)); - - return await this.createTreeItemsWithErrorHandling( - revisions, - 'invalidRevision', - re => new RevisionTreeItem(this, re), - re => re.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public async getRevision(context: IActionContext, name: string): Promise { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this]); - return await client.containerAppsRevisions.getRevision(this.parent.resourceGroupName, this.parent.name, name); - } -} diff --git a/src/tree/ScaleRuleGroupTreeItem.ts b/src/tree/ScaleRuleGroupTreeItem.ts deleted file mode 100644 index 9de06959e..000000000 --- a/src/tree/ScaleRuleGroupTreeItem.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ScaleRule } from "@azure/arm-appcontainers"; -import { AzExtParentTreeItem, AzExtTreeItem, AzureWizard, ICreateChildImplContext, IWizardOptions, nonNullProp, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { AddScaleRuleStep } from "../commands/scaling/addScaleRule/AddScaleRuleStep"; -import { IAddScaleRuleWizardContext } from "../commands/scaling/addScaleRule/IAddScaleRuleWizardContext"; -import { ScaleRuleNameStep } from "../commands/scaling/addScaleRule/ScaleRuleNameStep"; -import { ScaleRuleTypeStep } from "../commands/scaling/addScaleRule/ScaleRuleTypeStep"; -import { azResourceContextValue } from "../constants"; -import { localize } from "../utils/localize"; -import { treeUtils } from "../utils/treeUtils"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; -import { IAzureResourceTreeItem } from "./IAzureResourceTreeItem"; -import { ScaleRuleTreeItem } from "./ScaleRuleTreeItem"; -import { ScaleTreeItem } from "./ScaleTreeItem"; - -export class ScaleRuleGroupTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'scaleRules'; - public readonly contextValue: string = `${ScaleRuleGroupTreeItem.contextValue}|${azResourceContextValue}`; - public readonly parent: ScaleTreeItem; - - public label: string; - public data: ScaleRule[]; - - constructor(parent: ScaleTreeItem, data: ScaleRule[]) { - super(parent); - this.label = localize('scaleRules', 'Scale Rules'); - this.data = data; - } - - public get iconPath(): TreeItemIconPath { - return new ThemeIcon('symbol-constant'); - } - - public async createChildImpl(context: ICreateChildImplContext): Promise { - const scale: ScaleTreeItem = treeUtils.findNearestParent(this, ScaleTreeItem.contextValue); - const containerApp: ContainerAppTreeItem = treeUtils.findNearestParent(this, ContainerAppTreeItem.contextValue); - - const title: string = localize('addScaleRuleTitle', 'Add Scale Rule'); - const wizardContext: IAddScaleRuleWizardContext = { - ...context, containerApp, scale, scaleRuleGroup: this, - }; - const wizardOptions: IWizardOptions = { - title, - promptSteps: [new ScaleRuleNameStep(), new ScaleRuleTypeStep()], - executeSteps: [new AddScaleRuleStep()], - showLoadingPrompt: true - }; - const wizard: AzureWizard = new AzureWizard(wizardContext, wizardOptions); - await wizard.prompt(); - context.showCreatingTreeItem(nonNullProp(wizardContext, 'ruleName')); - await wizard.execute(); - return new ScaleRuleTreeItem(this, nonNullProp(wizardContext, "scaleRule")); - } - - public async loadMoreChildrenImpl(): Promise { - return this.createTreeItemsWithErrorHandling( - this.data, - 'invalidRule', - rule => new ScaleRuleTreeItem(this, rule), - _rule => localize('invalidScalingRule', 'Invalid Scaling Rule') - ); - } - - public hasMoreChildrenImpl(): boolean { - return false; - } -} diff --git a/src/tree/ScaleRuleTreeItem.ts b/src/tree/ScaleRuleTreeItem.ts deleted file mode 100644 index 6a4f1fe98..000000000 --- a/src/tree/ScaleRuleTreeItem.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ScaleRule } from "@azure/arm-appcontainers"; -import { AzExtTreeItem, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { azResourceContextValue } from "../constants"; -import { localize } from "../utils/localize"; -import { nonNullProp } from "../utils/nonNull"; -import { IAzureResourceTreeItem } from "./IAzureResourceTreeItem"; -import { ScaleRuleGroupTreeItem } from "./ScaleRuleGroupTreeItem"; - -export class ScaleRuleTreeItem extends AzExtTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'scaleRule'; - public readonly contextValue: string = `${ScaleRuleTreeItem.contextValue}|${azResourceContextValue}`; - public readonly parent: ScaleRuleGroupTreeItem; - public data: ScaleRule; - - public label: string; - - constructor(parent: ScaleRuleGroupTreeItem, data: ScaleRule) { - super(parent); - this.data = data; - this.label = nonNullProp(data, 'name'); - } - - public get description(): string { - if (this.data.http) return localize('http', "HTTP"); - else if (this.data.azureQueue) return localize('azureQueue', 'Azure Queue'); - else if (this.data.custom) return localize('custom', 'Custom'); - else return localize('unknown', 'Unknown'); - } - - public get iconPath(): TreeItemIconPath { - return new ThemeIcon('dash'); - } -} diff --git a/src/tree/ScaleTreeItem.ts b/src/tree/ScaleTreeItem.ts deleted file mode 100644 index f3a477201..000000000 --- a/src/tree/ScaleTreeItem.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Scale } from "@azure/arm-appcontainers"; -import { AzExtParentTreeItem, AzExtTreeItem, GenericTreeItem, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { azResourceContextValue } from "../constants"; -import { localize } from "../utils/localize"; -import { treeUtils } from "../utils/treeUtils"; -import { ContainerAppTreeItem } from "./ContainerAppTreeItem"; -import { IAzureResourceTreeItem } from "./IAzureResourceTreeItem"; -import { RevisionTreeItem } from "./RevisionTreeItem"; -import { ScaleRuleGroupTreeItem } from "./ScaleRuleGroupTreeItem"; - -export class ScaleTreeItem extends AzExtParentTreeItem implements IAzureResourceTreeItem { - public static contextValue: string = 'scale'; - public readonly contextValue: string = `${ScaleTreeItem.contextValue}|${azResourceContextValue}`; - public readonly parent: ContainerAppTreeItem | RevisionTreeItem; - public data: Scale; - - public label: string; - public minReplicas: string; - public maxReplicas: string; - - constructor(parent: ContainerAppTreeItem | RevisionTreeItem, data: Scale | undefined) { - super(parent); - this.label = localize('scale', 'Scaling'); - - this.data = data || {}; - this.minReplicas = String(this.data.minReplicas ?? 0); - this.maxReplicas = String(this.data.maxReplicas ?? this.data.minReplicas ?? 0); - } - - public get iconPath(): TreeItemIconPath { - return treeUtils.getIconPath('02887-icon-menu-Container-Scale'); - } - - public async loadMoreChildrenImpl(): Promise { - return [ - new GenericTreeItem(this, { label: localize('minMax', 'Min / max replicas'), description: `${this.minReplicas} / ${this.maxReplicas}`, contextValue: 'minMaxReplica', iconPath: new ThemeIcon('dash') }), - new ScaleRuleGroupTreeItem(this, this.data.rules ?? [])] - } - - public hasMoreChildrenImpl(): boolean { - return false; - } -} diff --git a/src/tree/SubscriptionTreeItem.ts b/src/tree/SubscriptionTreeItem.ts deleted file mode 100644 index 4755bebbb..000000000 --- a/src/tree/SubscriptionTreeItem.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ContainerAppsAPIClient, ManagedEnvironment } from "@azure/arm-appcontainers"; -import { LocationListStep, ResourceGroupCreateStep, SubscriptionTreeItemBase, uiUtils, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; -import { AzExtTreeItem, AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, ICreateChildImplContext } from '@microsoft/vscode-azext-utils'; -import { IManagedEnvironmentContext } from '../commands/createManagedEnvironment/IManagedEnvironmentContext'; -import { LogAnalyticsCreateStep } from '../commands/createManagedEnvironment/LogAnalyticsCreateStep'; -import { ManagedEnvironmentCreateStep } from '../commands/createManagedEnvironment/ManagedEnvironmentCreateStep'; -import { ManagedEnvironmentNameStep } from '../commands/createManagedEnvironment/ManagedEnvironmentNameStep'; -import { createActivityContext } from "../utils/activityUtils"; -import { createContainerAppsAPIClient } from '../utils/azureClients'; -import { localize } from '../utils/localize'; -import { nonNullProp } from '../utils/nonNull'; -import { ManagedEnvironmentTreeItem } from './ManagedEnvironmentTreeItem'; -import { ResolvedContainerEnvironmentResource } from "./ResolvedContainerAppsResource"; - -export class SubscriptionTreeItem extends SubscriptionTreeItemBase { - public readonly childTypeLabel: string = localize('ManagedEnvironment', 'Container Apps environment'); - private readonly _nextLink: string | undefined; - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - public async loadMoreChildrenImpl(_clearCache: boolean, context: IActionContext): Promise { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, this]); - const environments: ManagedEnvironment[] = await uiUtils.listAllIterator(client.managedEnvironments.listBySubscription()); - - return await this.createTreeItemsWithErrorHandling( - environments, - 'invalidManagedEnvironment', - ke => new ManagedEnvironmentTreeItem(this, new ResolvedContainerEnvironmentResource(this.subscription, ke)), - ke => ke.name - ); - } - - public static async createChild(context: ICreateChildImplContext, node: SubscriptionTreeItemBase): Promise { - const wizardContext: IManagedEnvironmentContext = { - ...context, - ...node.subscription, - ...(await createActivityContext()) - }; - - const title: string = localize('createManagedEnv', 'Create Container Apps environment'); - const promptSteps: AzureWizardPromptStep[] = []; - const executeSteps: AzureWizardExecuteStep[] = []; - - promptSteps.push(new ManagedEnvironmentNameStep()); - executeSteps.push(new VerifyProvidersStep(['Microsoft.App', 'Microsoft.OperationalInsights']), new ResourceGroupCreateStep(), new LogAnalyticsCreateStep(), new ManagedEnvironmentCreateStep()); - LocationListStep.addProviderForFiltering(wizardContext, 'Microsoft.App', 'managedEnvironments'); - LocationListStep.addStep(wizardContext, promptSteps); - - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title, - promptSteps, - executeSteps, - showLoadingPrompt: true - }); - - await wizard.prompt(); - const newManagedEnvName = nonNullProp(wizardContext, 'newManagedEnvironmentName'); - wizardContext.newResourceGroupName = newManagedEnvName; - wizardContext.activityTitle = localize('createNamedManagedEnv', 'Create Container Apps environment "{0}"', newManagedEnvName); - await wizard.execute(); - - const resolvedEnvironment = new ResolvedContainerEnvironmentResource(node.subscription, nonNullProp(wizardContext, 'managedEnvironment')); - return new ManagedEnvironmentTreeItem(node, resolvedEnvironment); - } -} diff --git a/src/tree/TreeItemState.ts b/src/tree/TreeItemState.ts new file mode 100644 index 000000000..d8eb46f81 --- /dev/null +++ b/src/tree/TreeItemState.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { createGenericItem } from '../utils/GenericItem'; +import { TreeElementBase } from './ContainerAppsBranchDataProvider'; + +interface TreeItemState { + /** + * Apply a temporary description to the tree item + */ + temporaryDescription?: string; + /** + * Set the tree item icon to a spinner + */ + spinner?: boolean; + /** + * Temporary children to be displayed + */ + temporaryChildren?: TreeElementBase[]; +} + +type ResourceGroupsItem = TreeElementBase & { id: string }; + +export class TreeItemStateStore implements vscode.Disposable { + private readonly store: Record | undefined> = {}; + private readonly disposables: vscode.Disposable[] = []; + private readonly onDidUpdateStateEmitter = new vscode.EventEmitter(); + private readonly onDidUpdateStateEvent: vscode.Event = this.onDidUpdateStateEmitter.event; + + /** + * Notify a resource that its children have changed. + */ + notifyChildrenChanged(id: string): void { + this.onDidUpdateStateEmitter.fire(id); + } + + wrapItemInStateHandling(item: ResourceGroupsItem, refresh: (item: ResourceGroupsItem) => void): ResourceGroupsItem { + const getTreeItem = item.getTreeItem.bind(item) as typeof item.getTreeItem; + item.getTreeItem = async () => { + const treeItem = await getTreeItem(); + if (item.id) { + return this.applyToTreeItem({ ...treeItem, id: item.id }); + } + return treeItem; + } + + if (item.getChildren) { + const getChildren = item.getChildren.bind(item) as typeof item.getChildren; + item.getChildren = async () => { + const children = await getChildren() ?? []; + + const state = this.getState(item.id); + if (state.temporaryChildren) { + children.unshift(...state.temporaryChildren); + } + + return children; + } + } + + this.onDidRequestRefresh(item.id, () => refresh(item)); + + return item; + } + + dispose(): void { + this.disposables.forEach((disposable) => { + disposable.dispose(); + }); + } + + async runWithTemporaryDescription(id: string, description: string, callback: () => Promise): Promise { + let result: T; + this.update(id, { ...this.getState(id), temporaryDescription: description, spinner: true }); + try { + result = await callback(); + } finally { + this.update(id, { ...this.getState(id), temporaryDescription: undefined, spinner: false }); + } + return result; + } + + private async runWithTemporaryChildren(id: string, child: TreeElementBase, callback: () => Promise): Promise { + let result: T; + this.update(id, { ...this.getState(id), temporaryChildren: [child] }); + try { + result = await callback(); + } finally { + this.update(id, { ...this.getState(id), temporaryChildren: undefined }); + } + return result; + } + + async showCreatingChild(id: string, label: string, callback: () => Promise): Promise { + return await this.runWithTemporaryChildren(id, createGenericItem({ + iconPath: new vscode.ThemeIcon('loading~spin'), + label, + contextValue: 'creatingChild', + }), async () => { + return await callback(); + }); + } + + private applyStateToTreeItem(state: Partial, treeItem: vscode.TreeItem): vscode.TreeItem { + + if (state.temporaryDescription) { + treeItem.description = state.temporaryDescription; + } + + if (state.spinner) { + treeItem.iconPath = new vscode.ThemeIcon('loading~spin'); + } + + return treeItem; + } + + private onDidRequestRefresh(id: string, callback: () => void): void { + this.disposables.push(this.onDidUpdateStateEvent((eventId: string) => { + if (eventId === id) { + callback(); + } + })); + } + + private applyToTreeItem(treeItem: vscode.TreeItem & { id: string }): vscode.TreeItem { + const state = this.getState(treeItem.id); + return this.applyStateToTreeItem(state, { ...treeItem }); + } + + private getState(id: string): Partial { + return this.store[id] ?? {}; + } + + private update(id: string, state: Partial): void { + this.store[id] = { ...this.getState(id), ...state }; + this.onDidUpdateStateEmitter.fire(id); + } +} diff --git a/src/tree/scaling/ScaleItem.ts b/src/tree/scaling/ScaleItem.ts new file mode 100644 index 000000000..07268b6a5 --- /dev/null +++ b/src/tree/scaling/ScaleItem.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { Revision, Scale } from "@azure/arm-appcontainers"; +import { nonNullValue } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription, ViewPropertiesModel } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { createGenericItem } from "../../utils/GenericItem"; +import { localize } from "../../utils/localize"; +import { treeUtils } from "../../utils/treeUtils"; +import { ContainerAppModel } from "../ContainerAppItem"; +import { ContainerAppsItem, TreeElementBase } from "../ContainerAppsBranchDataProvider"; +import { createScaleRuleGroupItem } from "./ScaleRuleGroupItem"; + +export class ScaleItem implements ContainerAppsItem { + constructor( + public readonly subscription: AzureSubscription, + public readonly containerApp: ContainerAppModel, + public readonly revision: Revision, + ) { } + + id: string = `${this.parentResource.id}/scale`; + + viewProperties: ViewPropertiesModel = { + data: this.scale, + label: `${this.parentResource.name} Scaling`, + }; + + get scale(): Scale { + return nonNullValue(this.revision?.template?.scale); + } + + get parentResource(): ContainerAppModel | Revision { + return this.revision?.name === this.containerApp.latestRevisionName ? this.containerApp : this.revision; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + label: localize('scaling', 'Scaling'), + contextValue: 'scale', + iconPath: treeUtils.getIconPath('02887-icon-menu-Container-Scale'), + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + async getChildren?(): Promise { + return [ + createGenericItem({ + label: localize('minMax', 'Min / max replicas'), + description: `${this.scale?.minReplicas ?? 0} / ${this.scale?.maxReplicas ?? 0}`, + contextValue: 'minMaxReplica', + iconPath: new ThemeIcon('dash'), + }), + createScaleRuleGroupItem(this.subscription, this.containerApp, this.revision), + ] + } +} diff --git a/src/tree/scaling/ScaleRuleGroupItem.ts b/src/tree/scaling/ScaleRuleGroupItem.ts new file mode 100644 index 000000000..a5914d994 --- /dev/null +++ b/src/tree/scaling/ScaleRuleGroupItem.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { Revision, ScaleRule } from "@azure/arm-appcontainers"; +import { nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeIcon, TreeItemCollapsibleState } from "vscode"; +import { localize } from "../../utils/localize"; +import { ContainerAppModel } from "../ContainerAppItem"; +import { RevisionsItemModel } from "../RevisionItem"; +import { createScaleRuleItem } from "./ScaleRuleItem"; + +export interface ScaleRuleGroupItem extends RevisionsItemModel { + scaleRules: ScaleRule[]; +} + +export function createScaleRuleGroupItem(subscription: AzureSubscription, containerApp: ContainerAppModel, revision: Revision): ScaleRuleGroupItem { + const scaleRules = nonNullValueAndProp(revision.template, 'scale').rules ?? []; + const parentResource = revision.name === containerApp.latestRevisionName ? containerApp : revision; + const id = `${parentResource.id}/scalerules`; + + return { + id, + subscription, + containerApp, + scaleRules, + viewProperties: { + data: scaleRules, + label: `${parentResource.name} Scale Rules`, + }, + revision, + getTreeItem: () => ({ + id, + label: localize('scaleRules', 'Scale Rules'), + iconPath: new ThemeIcon('symbol-constant'), + contextValue: 'scaleRules', + collapsibleState: TreeItemCollapsibleState.Collapsed, + }), + getChildren: async () => { + return scaleRules.map(scaleRule => createScaleRuleItem(subscription, containerApp, revision, scaleRule)); + }, + }; +} diff --git a/src/tree/scaling/ScaleRuleItem.ts b/src/tree/scaling/ScaleRuleItem.ts new file mode 100644 index 000000000..7854030d2 --- /dev/null +++ b/src/tree/scaling/ScaleRuleItem.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { Revision, ScaleRule } from "@azure/arm-appcontainers"; +import { nonNullProp } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { ThemeIcon, TreeItem } from "vscode"; +import { localize } from "../../utils/localize"; +import { ContainerAppModel } from "../ContainerAppItem"; +import { ContainerAppsItem } from "../ContainerAppsBranchDataProvider"; + +export interface ScaleRuleItem extends ContainerAppsItem { + scaleRule: ScaleRule; +} + +export function createScaleRuleItem(subscription: AzureSubscription, containerApp: ContainerAppModel, revision: Revision, scaleRule: ScaleRule): ScaleRuleItem { + const parentResource = revision.name === containerApp.latestRevisionName ? containerApp : revision; + + const id = `${parentResource.id}/${scaleRule.name}`; + + return { + id, + subscription, + containerApp, + scaleRule, + viewProperties: { + data: scaleRule, + label: `${parentResource.name} ${localize('scaleRule', 'Scale Rule')} ${scaleRule.name}`, + }, + getTreeItem: (): TreeItem => ({ + id, + label: nonNullProp(scaleRule, 'name'), + iconPath: new ThemeIcon('dash'), + contextValue: 'scaleRule', + description: getDescription(scaleRule), + }), + }; +} + +function getDescription(scaleRule: ScaleRule): string { + if (scaleRule.http) { + return localize('http', "HTTP"); + } else if (scaleRule.azureQueue) { + return localize('azureQueue', 'Azure Queue'); + } else if (scaleRule.custom) { + return localize('custom', 'Custom'); + } else { + return localize('unknown', 'Unknown'); + } +} diff --git a/src/utils/GenericItem.ts b/src/utils/GenericItem.ts new file mode 100644 index 000000000..8dd6cb7b3 --- /dev/null +++ b/src/utils/GenericItem.ts @@ -0,0 +1,29 @@ +import { IGenericTreeItemOptions } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { TreeElementBase } from '../tree/ContainerAppsBranchDataProvider'; + +export interface GenericItemOptions extends IGenericTreeItemOptions { + commandArgs?: unknown[]; +} + +export function createGenericItem(options: GenericItemOptions): TreeElementBase { + + let commandArgs = options.commandArgs; + const item = { + id: options.id, + getTreeItem(): vscode.TreeItem { + return { + ...options, + command: options.commandId ? { + title: '', + command: options.commandId, + arguments: commandArgs, + } : undefined, + } + } + }; + + commandArgs ??= [item]; + + return item; +} diff --git a/src/utils/activityUtils.ts b/src/utils/activityUtils.ts index 5b59a3e93..95eff9fe7 100644 --- a/src/utils/activityUtils.ts +++ b/src/utils/activityUtils.ts @@ -9,7 +9,7 @@ import { settingUtils } from "../utils/settingUtils"; export async function createActivityContext(): Promise { return { - registerActivity: async (activity) => ext.rgApi.registerActivity(activity), + registerActivity: async (activity) => ext.rgApiV2.activity.registerActivity(activity), suppressNotification: await settingUtils.getWorkspaceSetting('suppressActivityNotifications', undefined, 'azureResourceGroups'), }; } diff --git a/src/utils/azureClients.ts b/src/utils/azureClients.ts index 43f90f2f3..92b979c83 100644 --- a/src/utils/azureClients.ts +++ b/src/utils/azureClients.ts @@ -8,10 +8,17 @@ import { ContainerRegistryManagementClient, ContainerRegistryManagementModels } import { OperationalInsightsManagementClient } from '@azure/arm-operationalinsights'; import { ContainerRegistryClient, KnownContainerRegistryAudience } from '@azure/container-registry'; import { AzExtClientContext, createAzureClient, parseClientContext } from '@microsoft/vscode-azext-azureutils'; +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import { createSubscriptionContext } from "../tree/ContainerAppsBranchDataProvider"; // Lazy-load @azure packages to improve startup performance. // NOTE: The client is the only import that matters, the rest of the types disappear when compiled to JavaScript +export async function createContainerAppsClient(context: IActionContext, subscription: AzureSubscription): Promise { + return createContainerAppsAPIClient([context, createSubscriptionContext(subscription)]); +} + export async function createContainerAppsAPIClient(context: AzExtClientContext): Promise { return createAzureClient(context, (await import('@azure/arm-appcontainers')).ContainerAppsAPIClient) } diff --git a/src/utils/createPortalUrl.ts b/src/utils/createPortalUrl.ts new file mode 100644 index 000000000..93457bae5 --- /dev/null +++ b/src/utils/createPortalUrl.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { OpenInPortalOptions } from "@microsoft/vscode-azext-azureutils"; +import { AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; +import * as vscode from 'vscode'; + +// TODO move to shared package +export function createPortalUrl(subscription: AzureSubscription, id: string, options?: OpenInPortalOptions): vscode.Uri { + const queryPrefix: string = (options && options.queryPrefix) ? `?${options.queryPrefix}` : ''; + const url: string = `${subscription.environment.portalUrl}/${queryPrefix}#@${subscription.tenantId}/resource${id}`; + + return vscode.Uri.parse(url); +} diff --git a/src/utils/pickContainerApp.ts b/src/utils/pickContainerApp.ts new file mode 100644 index 000000000..ab0bc8447 --- /dev/null +++ b/src/utils/pickContainerApp.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { AzExtResourceType, azureResourceExperience, IActionContext } from "@microsoft/vscode-azext-utils"; +import { ext } from "../extensionVariables"; +import { ContainerAppItem } from "../tree/ContainerAppItem"; + +// TODO: support creating a new container app from picker +export async function pickContainerApp(context: IActionContext): Promise { + return await azureResourceExperience(context, ext.rgApiV2.resources.azureResourceTreeDataProvider, AzExtResourceType.ContainerAppsEnvironment, { + include: ContainerAppItem.contextValueRegExp, + }); +} diff --git a/src/utils/pickEnvironment.ts b/src/utils/pickEnvironment.ts new file mode 100644 index 000000000..ccb7a4369 --- /dev/null +++ b/src/utils/pickEnvironment.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { AzExtResourceType, azureResourceExperience, IActionContext } from "@microsoft/vscode-azext-utils"; +import { ext } from "../extensionVariables"; +import { ManagedEnvironmentItem } from "../tree/ManagedEnvironmentItem"; + +export async function pickEnvironment(context: IActionContext): Promise { + return await azureResourceExperience(context, ext.rgApiV2.resources.azureResourceTreeDataProvider, AzExtResourceType.ContainerAppsEnvironment); +}