From 70b1351bd51d2d7506f89a2365456eab2b15f9ef Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 16 Jan 2024 15:45:18 -0500 Subject: [PATCH] feat(connections): add edit connection context menu action VSCODE-406 (#655) --- README.md | 1 - package-lock.json | 240 +++++++++--------- package.json | 25 +- src/commands/index.ts | 1 + src/connectionController.ts | 121 ++++++--- src/explorer/collectionTreeItem.ts | 2 +- src/explorer/connectionTreeItem.ts | 4 +- src/explorer/databaseTreeItem.ts | 2 +- src/explorer/streamProcessorTreeItem.ts | 2 +- src/mdbExtensionController.ts | 57 ++++- src/storage/connectionStorage.ts | 124 +++------ src/telemetry/telemetryService.ts | 7 + src/test/suite/connectionController.test.ts | 128 +++++++--- .../suite/explorer/databaseTreeItem.test.ts | 4 +- src/test/suite/mdbExtensionController.test.ts | 2 +- src/test/suite/oidc.test.ts | 6 +- .../suite/storage/connectionStorage.test.ts | 79 +++--- .../views/webview-app/overview-page.test.tsx | 60 ++++- .../suite/views/webviewController.test.ts | 236 ++++++----------- src/views/webview-app/connection-form.tsx | 38 ++- .../extension-app-message-constants.ts | 28 +- src/views/webview-app/overview-page.tsx | 15 +- src/views/webview-app/use-connection-form.ts | 195 ++++++++++++-- src/views/webview-app/vscode-api.ts | 13 +- src/views/webviewController.ts | 81 ++++-- 25 files changed, 887 insertions(+), 584 deletions(-) diff --git a/README.md b/README.md index 0168c911e..e621e8404 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,6 @@ If you use Terraform to manage your infrastructure, MongoDB for VS Code helps yo | `mdb.persistOIDCTokens` | Remain logged in when using the MONGODB-OIDC authentication mechanism for MongoDB server connection. Access tokens are encrypted using the system keychain before being stored. | `true` | | `mdb.showOIDCDeviceAuthFlow` | Opt-in and opt-out for diagnostic and telemetry collection. | `true` | | `mdb.excludeFromPlaygroundsSearch` | Exclude files and folders while searching for playground files in the current workspace. | Refer to [`package.json`](https://github.com/mongodb-js/vscode/blob/7b10092db4c8c10c4aa9c45b443c8ed3d5f37d5c/package.json) | -| `mdb.connectionSaving.` `hideOptionToChooseWhereToSaveNewConnections` | When a connection is added, a prompt is shown that let's the user decide where the new connection should be saved. When this setting is checked, the prompt is not shown and the default connection saving location setting is used. | `true` | | `mdb.connectionSaving.` `defaultConnectionSavingLocation` | When the setting that hides the option to choose where to save new connections is checked, this setting sets if and where new connections are saved. | `Global` | | `mdb.useDefaultTemplateForPlayground` | Choose whether to use the default template for playground files or to start with an empty playground editor. | `true` | | `mdb.uniqueObjectIdPerCursor` | The default behavior is to generate a single ObjectId and insert it on all cursors. Set to true to generate a unique ObjectId per cursor instead. | `false` | diff --git a/package-lock.json b/package-lock.json index 36cdbe838..0b7a0e867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@babel/parser": "^7.22.6", "@babel/traverse": "^7.23.2", "@mongodb-js/compass-components": "^1.20.0", - "@mongodb-js/connection-form": "^1.20.4", + "@mongodb-js/connection-form": "^1.22.2", "@mongodb-js/connection-info": "^0.1.1", "@mongodb-js/mongodb-constants": "^0.7.1", "@mongosh/browser-runtime-electron": "^2.0.2", @@ -1572,9 +1572,9 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.11.1.tgz", - "integrity": "sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz", + "integrity": "sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -1589,12 +1589,12 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.2.tgz", - "integrity": "sha512-tjoi4MCWDNxgIpoLZ7+tezdS9OEB6pkiDKhfKx9ReJ/XBcs2G2RXIu+/FxXBlWsPTsz6C9q/r4gjzrsxpcnqCQ==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz", + "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.2.0", + "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.1.0" } @@ -1623,12 +1623,12 @@ } }, "node_modules/@codemirror/language": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.3.tgz", - "integrity": "sha512-qq48pYzoi6ldYWV/52+Z9Ou6QouVI+8YwvxFbUypI33NbjG2UeRHKENRyhwljTTiOqjQ33FjyZj6EREQ9apAOQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.0.tgz", + "integrity": "sha512-2vaNn9aPGCRFKWcHPFksctzJ8yS5p7YoaT+jHpc0UGKzNuAIx4qy6R5wiqbP+heEEdyaABA582mNqSHzSoYdmg==", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", @@ -1646,16 +1646,16 @@ } }, "node_modules/@codemirror/state": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.3.3.tgz", - "integrity": "sha512-0wufKcTw2dEwEaADajjHf6hBy1sh3M6V0e+q4JKIhLuiMSe5td5HOWpUdvKth1fT1M9VYOboajoBHpkCd7PG7A==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz", + "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==" }, "node_modules/@codemirror/view": { - "version": "6.22.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.22.1.tgz", - "integrity": "sha512-38BRn1nPqZqiHbmWfI8zri23IbRVbmSpSmh1E/Ysvc+lIGGdBC17K8zlK7ZU6fhfy9x4De9Zyj5JQqScPq5DkA==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.23.0.tgz", + "integrity": "sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==", "dependencies": { - "@codemirror/state": "^6.1.4", + "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } @@ -4507,9 +4507,9 @@ "integrity": "sha512-AsvPlbvF7CERiZbAQR8hy3lAJ2/rieXI3cO0jsOwV8ztDqYNotKAdLujyr/NviudrRUenYiXrLizIKVlSPUMuA==" }, "node_modules/@lezer/common": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.2.tgz", - "integrity": "sha512-V+GqBsga5+cQJMfM0GdnHmg4DgWvLzgMWjbldBg0+jC3k9Gu6nJNZDLJxXEBT1Xj8KhRN4jmbC5CY7SIL++sVw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.0.tgz", + "integrity": "sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==" }, "node_modules/@lezer/highlight": { "version": "1.2.0", @@ -4520,19 +4520,20 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.10.tgz", - "integrity": "sha512-XJu3fZjHVVjJcRS7kHdwBO50irXc4H8rQwgm6SmT3Y8IHWk7WzpaLsaR2vdr/jSk/J4pQhXc1WLul7jVdxC+0Q==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.12.tgz", + "integrity": "sha512-kwO5MftUiyfKBcECMEDc4HYnc10JME9kTJNPVoCXqJj/Y+ASWF0rgstORi3BThlQI6SoPSshrK5TjuiLFnr29A==", "dependencies": { "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "node_modules/@lezer/json": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.1.tgz", - "integrity": "sha512-nkVC27qiEZEjySbi6gQRuMwa2sDu2PtfjSgz0A4QF81QyRGm3kb2YRzLcOPcTEtmcwvrX/cej7mlhbwViA4WJw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", "dependencies": { + "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } @@ -4565,9 +4566,9 @@ } }, "node_modules/@mongodb-js/compass-components": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.20.2.tgz", - "integrity": "sha512-EHWngr+15ga8wmuxt7HaqVXJh1YQuAE8ISRqzRborC8mEb+eTC7QxadwlQx4tny4sMKK1IjZPowDK3N2/3ig+A==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.21.2.tgz", + "integrity": "sha512-49zX5T5VXvLBcuU/SvPks/nbJyGy5pUCKBpaUV3Mb2ycIa8uj2cJvnte8YcPPDRn5dpcTM1Iar0NhqD8rjiPBQ==", "dependencies": { "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", @@ -4618,7 +4619,7 @@ "@react-stately/tooltip": "^3.0.5", "bson": "^6.2.0", "focus-trap-react": "^8.4.2", - "hadron-document": "^8.4.4", + "hadron-document": "^8.4.7", "hadron-type-checker": "^7.1.1", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", @@ -4648,9 +4649,9 @@ "integrity": "sha512-AsvPlbvF7CERiZbAQR8hy3lAJ2/rieXI3cO0jsOwV8ztDqYNotKAdLujyr/NviudrRUenYiXrLizIKVlSPUMuA==" }, "node_modules/@mongodb-js/compass-editor": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.19.2.tgz", - "integrity": "sha512-BBaMIzTjwgddcfFdrH/PbBcRIOgqQZQkOLGdj/48s3ga+aZcnweytTNw3z0mtVrzCaWtZPInhrh958JGLOvGxA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.20.2.tgz", + "integrity": "sha512-liiP6yrvpLSqpgkJ73KzkpbUIU7/CiU78Ldl381UbFkFjclq+pe6DXMRXwvCdBhcu+l1DoIPMFDz2ql93e6/4Q==", "dependencies": { "@codemirror/autocomplete": "^6.4.0", "@codemirror/commands": "^6.1.2", @@ -4661,7 +4662,7 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.1.3", - "@mongodb-js/compass-components": "^1.20.2", + "@mongodb-js/compass-components": "^1.21.2", "@mongodb-js/mongodb-constants": "^0.8.7", "polished": "^4.2.2", "prettier": "^2.7.1" @@ -4671,9 +4672,9 @@ } }, "node_modules/@mongodb-js/compass-editor/node_modules/@mongodb-js/mongodb-constants": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.8.8.tgz", - "integrity": "sha512-Exdpz+U+iv2qFZ6i7UTaxFbzSjhu3OuSiBUK/sbInCwlOWSO9KOczzZBYkmqvJBCksv9z4gdSEOT/b5wvCDndQ==", + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.8.9.tgz", + "integrity": "sha512-A9nSWucJylfb4fp+10Gzz4D2hAUKCuVSXs0XChUL/nrJIYsqoL9h5IZmytrGmKbcJ01yGSxatUJJicdLtlXPSQ==", "dependencies": { "semver": "^7.5.4" } @@ -4715,21 +4716,22 @@ } }, "node_modules/@mongodb-js/connection-form": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/connection-form/-/connection-form-1.20.4.tgz", - "integrity": "sha512-ZshFg3Glzr+DSCdyMYR2ieS2MDn73gt5eD+sKGaCYKHzQcu1G0Li0ocpsMdmnkNvZafyHruyg1IvgIyMHx6e+g==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/connection-form/-/connection-form-1.22.2.tgz", + "integrity": "sha512-bRQKka5LtYnQxuOfo08+tmFyAI12SkHE2EjlPzfFB8Zrn++O8BqfH88Nt4wXSMMnfH2gqWfsruFquMKmW+Favg==", "dependencies": { - "@mongodb-js/compass-components": "^1.20.2", - "@mongodb-js/compass-editor": "^0.19.2", + "@mongodb-js/compass-components": "^1.21.2", + "@mongodb-js/compass-editor": "^0.20.2", + "@mongodb-js/connection-info": "^0.1.2", "@testing-library/react-hooks": "^7.0.2", "lodash": "^4.17.21", "mongodb-build-info": "^1.7.0", "mongodb-connection-string-url": "^2.6.0", - "mongodb-query-parser": "^4.0.0" + "mongodb-query-parser": "^4.0.2" }, "peerDependencies": { - "@mongodb-js/compass-components": "^1.20.2", - "@mongodb-js/compass-editor": "^0.19.2", + "@mongodb-js/compass-components": "^1.21.2", + "@mongodb-js/compass-editor": "^0.20.2", "react": "^17.0.2" } }, @@ -4748,26 +4750,20 @@ } }, "node_modules/@mongodb-js/connection-form/node_modules/mongodb-query-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mongodb-query-parser/-/mongodb-query-parser-4.0.0.tgz", - "integrity": "sha512-3HjuFfkUH5OQCi8/qkT/ljMfp8tMOY0Yx9sCoNGEkQ7Utz2eR/IxbrkGs+SnsjwPCITc49d6dAm+/pFeGf0c1Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/mongodb-query-parser/-/mongodb-query-parser-4.0.2.tgz", + "integrity": "sha512-GajCLw8/Ut0mlIW1msU36Pvyg/FRc0LPyp8aNzfafzNrDwz2ZOJmwtmNAkckuXG3Iqnj0ZZOouhIo4E4rjXcBQ==", "dependencies": { - "debug": "^4.2.0", + "debug": "^4.3.4", "ejson-shell-parser": "^2.0.0", - "javascript-stringify": "^2.0.1", - "lodash": "^4.17.15" - }, - "engines": { - "node": ">= 16.17.0" - }, - "peerDependencies": { - "bson": "^5 || ^6" + "javascript-stringify": "^2.1.0", + "lodash": "^4.17.21" } }, "node_modules/@mongodb-js/connection-info": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/connection-info/-/connection-info-0.1.1.tgz", - "integrity": "sha512-ULZ/N3KVKfuAhKV1JfDi/okIkJ3pEarvVUZTgatkVRqiI54uG3B02RLHfg6iKgeJGO9+HhSJNCXk3x5PiLAs/w==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/connection-info/-/connection-info-0.1.2.tgz", + "integrity": "sha512-SjEsSJ+j5X64xrLRU+g03lewIQCUNLB8UDZMFzEvEpWdRIYPf0lKEBmQ/NIrIyij/qaRzmw2avWFAqvxk3oUqQ==", "dependencies": { "bson": "^6.2.0", "lodash": "^4.17.21", @@ -13089,9 +13085,9 @@ } }, "node_modules/hadron-document": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.4.4.tgz", - "integrity": "sha512-5Ke77wtP3FO97CynYmoLIub2k/mxdWjoGZge1vo9EO1lnHVH1Bxap0uJ461vGA/emBY336on5B7Q7bwDmt6VEw==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.4.7.tgz", + "integrity": "sha512-6nxghzGKDvRJ91XtNaAn6VYgJThhhD5ohwhGua4w7sCa524+PkasAUS7EOLWGcp5tlWP+mAyBlVtUf/r4XruGw==", "dependencies": { "bson": "^6.2.0", "debug": "^4.2.0", @@ -24995,9 +24991,9 @@ } }, "@codemirror/autocomplete": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.11.1.tgz", - "integrity": "sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz", + "integrity": "sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==", "requires": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -25006,12 +25002,12 @@ } }, "@codemirror/commands": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.2.tgz", - "integrity": "sha512-tjoi4MCWDNxgIpoLZ7+tezdS9OEB6pkiDKhfKx9ReJ/XBcs2G2RXIu+/FxXBlWsPTsz6C9q/r4gjzrsxpcnqCQ==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz", + "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==", "requires": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.2.0", + "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.1.0" } @@ -25040,12 +25036,12 @@ } }, "@codemirror/language": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.3.tgz", - "integrity": "sha512-qq48pYzoi6ldYWV/52+Z9Ou6QouVI+8YwvxFbUypI33NbjG2UeRHKENRyhwljTTiOqjQ33FjyZj6EREQ9apAOQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.0.tgz", + "integrity": "sha512-2vaNn9aPGCRFKWcHPFksctzJ8yS5p7YoaT+jHpc0UGKzNuAIx4qy6R5wiqbP+heEEdyaABA582mNqSHzSoYdmg==", "requires": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", @@ -25063,16 +25059,16 @@ } }, "@codemirror/state": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.3.3.tgz", - "integrity": "sha512-0wufKcTw2dEwEaADajjHf6hBy1sh3M6V0e+q4JKIhLuiMSe5td5HOWpUdvKth1fT1M9VYOboajoBHpkCd7PG7A==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz", + "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==" }, "@codemirror/view": { - "version": "6.22.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.22.1.tgz", - "integrity": "sha512-38BRn1nPqZqiHbmWfI8zri23IbRVbmSpSmh1E/Ysvc+lIGGdBC17K8zlK7ZU6fhfy9x4De9Zyj5JQqScPq5DkA==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.23.0.tgz", + "integrity": "sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==", "requires": { - "@codemirror/state": "^6.1.4", + "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } @@ -27583,9 +27579,9 @@ } }, "@lezer/common": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.2.tgz", - "integrity": "sha512-V+GqBsga5+cQJMfM0GdnHmg4DgWvLzgMWjbldBg0+jC3k9Gu6nJNZDLJxXEBT1Xj8KhRN4jmbC5CY7SIL++sVw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.0.tgz", + "integrity": "sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==" }, "@lezer/highlight": { "version": "1.2.0", @@ -27596,19 +27592,20 @@ } }, "@lezer/javascript": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.10.tgz", - "integrity": "sha512-XJu3fZjHVVjJcRS7kHdwBO50irXc4H8rQwgm6SmT3Y8IHWk7WzpaLsaR2vdr/jSk/J4pQhXc1WLul7jVdxC+0Q==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.12.tgz", + "integrity": "sha512-kwO5MftUiyfKBcECMEDc4HYnc10JME9kTJNPVoCXqJj/Y+ASWF0rgstORi3BThlQI6SoPSshrK5TjuiLFnr29A==", "requires": { "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "@lezer/json": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.1.tgz", - "integrity": "sha512-nkVC27qiEZEjySbi6gQRuMwa2sDu2PtfjSgz0A4QF81QyRGm3kb2YRzLcOPcTEtmcwvrX/cej7mlhbwViA4WJw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", "requires": { + "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } @@ -27635,9 +27632,9 @@ } }, "@mongodb-js/compass-components": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.20.2.tgz", - "integrity": "sha512-EHWngr+15ga8wmuxt7HaqVXJh1YQuAE8ISRqzRborC8mEb+eTC7QxadwlQx4tny4sMKK1IjZPowDK3N2/3ig+A==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.21.2.tgz", + "integrity": "sha512-49zX5T5VXvLBcuU/SvPks/nbJyGy5pUCKBpaUV3Mb2ycIa8uj2cJvnte8YcPPDRn5dpcTM1Iar0NhqD8rjiPBQ==", "requires": { "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", @@ -27688,7 +27685,7 @@ "@react-stately/tooltip": "^3.0.5", "bson": "^6.2.0", "focus-trap-react": "^8.4.2", - "hadron-document": "^8.4.4", + "hadron-document": "^8.4.7", "hadron-type-checker": "^7.1.1", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", @@ -27717,9 +27714,9 @@ } }, "@mongodb-js/compass-editor": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.19.2.tgz", - "integrity": "sha512-BBaMIzTjwgddcfFdrH/PbBcRIOgqQZQkOLGdj/48s3ga+aZcnweytTNw3z0mtVrzCaWtZPInhrh958JGLOvGxA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.20.2.tgz", + "integrity": "sha512-liiP6yrvpLSqpgkJ73KzkpbUIU7/CiU78Ldl381UbFkFjclq+pe6DXMRXwvCdBhcu+l1DoIPMFDz2ql93e6/4Q==", "requires": { "@codemirror/autocomplete": "^6.4.0", "@codemirror/commands": "^6.1.2", @@ -27730,16 +27727,16 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.1.3", - "@mongodb-js/compass-components": "^1.20.2", + "@mongodb-js/compass-components": "^1.21.2", "@mongodb-js/mongodb-constants": "^0.8.7", "polished": "^4.2.2", "prettier": "^2.7.1" }, "dependencies": { "@mongodb-js/mongodb-constants": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.8.8.tgz", - "integrity": "sha512-Exdpz+U+iv2qFZ6i7UTaxFbzSjhu3OuSiBUK/sbInCwlOWSO9KOczzZBYkmqvJBCksv9z4gdSEOT/b5wvCDndQ==", + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.8.9.tgz", + "integrity": "sha512-A9nSWucJylfb4fp+10Gzz4D2hAUKCuVSXs0XChUL/nrJIYsqoL9h5IZmytrGmKbcJ01yGSxatUJJicdLtlXPSQ==", "requires": { "semver": "^7.5.4" } @@ -27778,17 +27775,18 @@ } }, "@mongodb-js/connection-form": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/connection-form/-/connection-form-1.20.4.tgz", - "integrity": "sha512-ZshFg3Glzr+DSCdyMYR2ieS2MDn73gt5eD+sKGaCYKHzQcu1G0Li0ocpsMdmnkNvZafyHruyg1IvgIyMHx6e+g==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/connection-form/-/connection-form-1.22.2.tgz", + "integrity": "sha512-bRQKka5LtYnQxuOfo08+tmFyAI12SkHE2EjlPzfFB8Zrn++O8BqfH88Nt4wXSMMnfH2gqWfsruFquMKmW+Favg==", "requires": { - "@mongodb-js/compass-components": "^1.20.2", - "@mongodb-js/compass-editor": "^0.19.2", + "@mongodb-js/compass-components": "^1.21.2", + "@mongodb-js/compass-editor": "^0.20.2", + "@mongodb-js/connection-info": "^0.1.2", "@testing-library/react-hooks": "^7.0.2", "lodash": "^4.17.21", "mongodb-build-info": "^1.7.0", "mongodb-connection-string-url": "^2.6.0", - "mongodb-query-parser": "^4.0.0" + "mongodb-query-parser": "^4.0.2" }, "dependencies": { "ejson-shell-parser": { @@ -27800,22 +27798,22 @@ } }, "mongodb-query-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mongodb-query-parser/-/mongodb-query-parser-4.0.0.tgz", - "integrity": "sha512-3HjuFfkUH5OQCi8/qkT/ljMfp8tMOY0Yx9sCoNGEkQ7Utz2eR/IxbrkGs+SnsjwPCITc49d6dAm+/pFeGf0c1Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/mongodb-query-parser/-/mongodb-query-parser-4.0.2.tgz", + "integrity": "sha512-GajCLw8/Ut0mlIW1msU36Pvyg/FRc0LPyp8aNzfafzNrDwz2ZOJmwtmNAkckuXG3Iqnj0ZZOouhIo4E4rjXcBQ==", "requires": { - "debug": "^4.2.0", + "debug": "^4.3.4", "ejson-shell-parser": "^2.0.0", - "javascript-stringify": "^2.0.1", - "lodash": "^4.17.15" + "javascript-stringify": "^2.1.0", + "lodash": "^4.17.21" } } } }, "@mongodb-js/connection-info": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/connection-info/-/connection-info-0.1.1.tgz", - "integrity": "sha512-ULZ/N3KVKfuAhKV1JfDi/okIkJ3pEarvVUZTgatkVRqiI54uG3B02RLHfg6iKgeJGO9+HhSJNCXk3x5PiLAs/w==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/connection-info/-/connection-info-0.1.2.tgz", + "integrity": "sha512-SjEsSJ+j5X64xrLRU+g03lewIQCUNLB8UDZMFzEvEpWdRIYPf0lKEBmQ/NIrIyij/qaRzmw2avWFAqvxk3oUqQ==", "requires": { "bson": "^6.2.0", "lodash": "^4.17.21", @@ -34309,9 +34307,9 @@ } }, "hadron-document": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.4.4.tgz", - "integrity": "sha512-5Ke77wtP3FO97CynYmoLIub2k/mxdWjoGZge1vo9EO1lnHVH1Bxap0uJ461vGA/emBY336on5B7Q7bwDmt6VEw==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.4.7.tgz", + "integrity": "sha512-6nxghzGKDvRJ91XtNaAn6VYgJThhhD5ohwhGua4w7sCa524+PkasAUS7EOLWGcp5tlWP+mAyBlVtUf/r4XruGw==", "requires": { "bson": "^6.2.0", "debug": "^4.2.0", diff --git a/package.json b/package.json index 303f56d04..80b8a4e25 100644 --- a/package.json +++ b/package.json @@ -274,6 +274,10 @@ "command": "mdb.treeItemRemoveConnection", "title": "Remove Connection..." }, + { + "command": "mdb.editConnection", + "title": "Edit Connection..." + }, { "command": "mdb.addDatabase", "title": "Add Database...", @@ -481,6 +485,11 @@ "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", "group": "3@1" }, + { + "command": "mdb.editConnection", + "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "group": "3@2" + }, { "command": "mdb.copyConnectionString", "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", @@ -511,6 +520,11 @@ "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", "group": "2@1" }, + { + "command": "mdb.editConnection", + "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "group": "2@2" + }, { "command": "mdb.copyConnectionString", "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", @@ -746,6 +760,10 @@ "command": "mdb.renameConnection", "when": "false" }, + { + "command": "mdb.editConnection", + "when": "false" + }, { "command": "mdb.treeItemRemoveConnection", "when": "false" @@ -1008,11 +1026,6 @@ "default": true, "description": "Allow the collection of anonymous diagnostic and usage telemetry data to help improve the product." }, - "mdb.connectionSaving.hideOptionToChooseWhereToSaveNewConnections": { - "type": "boolean", - "default": true, - "description": "When a connection is added, a prompt is shown that let's the user decide where the new connection should be saved. When this setting is checked, the prompt is not shown and the default connection saving location setting is used." - }, "mdb.connectionSaving.defaultConnectionSavingLocation": { "type": "string", "enum": [ @@ -1050,7 +1063,7 @@ "@babel/parser": "^7.22.6", "@babel/traverse": "^7.23.2", "@mongodb-js/compass-components": "^1.20.0", - "@mongodb-js/connection-form": "^1.20.4", + "@mongodb-js/connection-form": "^1.22.2", "@mongodb-js/connection-info": "^0.1.1", "@mongodb-js/mongodb-constants": "^0.7.1", "@mongosh/browser-runtime-electron": "^2.0.2", diff --git a/src/commands/index.ts b/src/commands/index.ts index ede46c20c..8e5e3c989 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -41,6 +41,7 @@ enum EXTENSION_COMMANDS { MDB_CONNECT_TO_CONNECTION_TREE_VIEW = 'mdb.connectToConnectionTreeItem', MDB_CREATE_PLAYGROUND_FROM_TREE_VIEW = 'mdb.createNewPlaygroundFromTreeView', MDB_DISCONNECT_FROM_CONNECTION_TREE_VIEW = 'mdb.disconnectFromConnectionTreeItem', + MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', MDB_COPY_CONNECTION_STRING = 'mdb.copyConnectionString', MDB_REMOVE_CONNECTION_TREE_VIEW = 'mdb.treeItemRemoveConnection', diff --git a/src/connectionController.ts b/src/connectionController.ts index a661f2ffd..259c03387 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -251,15 +251,13 @@ export default class ConnectionController { ); try { - const connectResult = await this.saveNewConnectionAndConnect( - { - id: uuidv4(), - connectionOptions: { - connectionString: connectionStringData.toString(), - }, + const connectResult = await this.saveNewConnectionAndConnect({ + connectionId: uuidv4(), + connectionOptions: { + connectionString: connectionStringData.toString(), }, - ConnectionTypes.CONNECTION_STRING - ); + connectionType: ConnectionTypes.CONNECTION_STRING, + }); return connectResult.successfullyConnected; } catch (error) { @@ -282,27 +280,25 @@ export default class ConnectionController { ); } - async saveNewConnectionAndConnect( - connection: { - connectionOptions: ConnectionOptions; - id: string; - }, - connectionType: ConnectionTypes - ): Promise { - const savedConnectionWithoutSecrets = - await this._connectionStorage.saveNewConnection(connection); + async saveNewConnectionAndConnect({ + connectionOptions, + connectionId, + connectionType, + }: { + connectionOptions: ConnectionOptions; + connectionId: string; + connectionType: ConnectionTypes; + }): Promise { + const connection = this._connectionStorage.createNewConnection({ + connectionId, + connectionOptions, + }); - this._connections[savedConnectionWithoutSecrets.id] = { - ...savedConnectionWithoutSecrets, - connectionOptions: connection.connectionOptions, // The connection options with secrets. - }; + await this._connectionStorage.saveConnection(connection); - log.info( - 'Connect called to connect to instance', - savedConnectionWithoutSecrets.name - ); + this._connections[connection.id] = cloneDeep(connection); - return this._connect(savedConnectionWithoutSecrets.id, connectionType); + return this._connect(connection.id, connectionType); } // eslint-disable-next-line complexity @@ -310,6 +306,11 @@ export default class ConnectionController { connectionId: string, connectionType: ConnectionTypes ): Promise { + log.info( + 'Connect called to connect to instance', + this._connections[connectionId]?.name || 'empty connection name' + ); + // Cancel the current connection attempt if we're connecting. this._connectionAttempt?.cancelConnectionAttempt(); @@ -538,7 +539,9 @@ export default class ConnectionController { this._connectionAttempt?.cancelConnectionAttempt(); } - async connectWithConnectionId(connectionId: string): Promise { + async connectWithConnectionId( + connectionId: string + ): Promise { if (!this._connections[connectionId]) { throw new Error('Connection not found.'); } @@ -546,14 +549,20 @@ export default class ConnectionController { try { await this._connect(connectionId, ConnectionTypes.CONNECTION_ID); - return true; + return { + successfullyConnected: true, + connectionErrorMessage: '', + }; } catch (error) { log.error('Failed to connect by a connection id', error); const printableError = formatError(error); void vscode.window.showErrorMessage( `Unable to connect: ${printableError.message}` ); - return false; + return { + successfullyConnected: false, + connectionErrorMessage: '', + }; } } @@ -598,7 +607,7 @@ export default class ConnectionController { } catch (error) { // Show an error, however we still reset the active connection to free up the extension. void vscode.window.showErrorMessage( - 'An error occured while disconnecting from the current connection.' + 'An error occurred while disconnecting from the current connection.' ); } @@ -692,6 +701,41 @@ export default class ConnectionController { return this.removeMongoDBConnection(connectionIdToRemove); } + async updateConnection({ + connectionId, + connectionOptions, + }: { + connectionId: string; + connectionOptions: ConnectionOptions; + }): Promise { + if (!this._connections[connectionId]) { + throw new Error('Cannot find connection to update.'); + } + + this._connections[connectionId] = { + ...this._connections[connectionId], + connectionOptions, + }; + await this._connectionStorage.saveConnection( + this._connections[connectionId] + ); + } + + async updateConnectionAndConnect({ + connectionId, + connectionOptions, + }: { + connectionId: string; + connectionOptions: ConnectionOptions; + }): Promise { + await this.updateConnection({ + connectionId, + connectionOptions, + }); + + return await this.connectWithConnectionId(connectionId); + } + async renameConnection(connectionId: string): Promise { let inputtedConnectionName: string | undefined; @@ -789,6 +833,20 @@ export default class ConnectionController { : ''; } + getConnectionConnectionOptions( + connectionId: string + ): ConnectionOptions | undefined { + const connectionStringWithoutAppName = new ConnectionString( + this._connections[connectionId]?.connectionOptions.connectionString + ); + connectionStringWithoutAppName.searchParams.delete('appname'); + + return { + ...this._connections[connectionId]?.connectionOptions, + connectionString: connectionStringWithoutAppName.toString(), + }; + } + _getConnectionStringWithProxy({ url, options, @@ -982,8 +1040,9 @@ export default class ConnectionController { return true; } - return this.connectWithConnectionId( + const { successfullyConnected } = await this.connectWithConnectionId( selectedQuickPickItem.data.connectionId ); + return successfullyConnected; } } diff --git a/src/explorer/collectionTreeItem.ts b/src/explorer/collectionTreeItem.ts index c525fcb6e..75d2709b6 100644 --- a/src/explorer/collectionTreeItem.ts +++ b/src/explorer/collectionTreeItem.ts @@ -366,7 +366,7 @@ export default class CollectionTreeItem }); } catch (error) { return Promise.reject( - new Error(`An error occured parsing the collection name: ${error}`) + new Error(`An error occurred parsing the collection name: ${error}`) ); } diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index a7f7612fc..7eecddc72 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -267,11 +267,11 @@ export default class ConnectionTreeItem // If we aren't the active connection, we reconnect. try { - const connectSuccess = + const { successfullyConnected } = await this._connectionController.connectWithConnectionId( this.connectionId ); - return connectSuccess; + return successfullyConnected; } catch (err) { this.isExpanded = false; void vscode.window.showErrorMessage( diff --git a/src/explorer/databaseTreeItem.ts b/src/explorer/databaseTreeItem.ts index 08d5695a6..24f96f64f 100644 --- a/src/explorer/databaseTreeItem.ts +++ b/src/explorer/databaseTreeItem.ts @@ -214,7 +214,7 @@ export default class DatabaseTreeItem }); } catch (e) { return Promise.reject( - new Error(`An error occured parsing the database name: ${e}`) + new Error(`An error occurred parsing the database name: ${e}`) ); } diff --git a/src/explorer/streamProcessorTreeItem.ts b/src/explorer/streamProcessorTreeItem.ts index a728cb50e..583e934fd 100644 --- a/src/explorer/streamProcessorTreeItem.ts +++ b/src/explorer/streamProcessorTreeItem.ts @@ -127,7 +127,7 @@ export default class StreamProcessorTreeItem }); } catch (e) { return Promise.reject( - new Error(`An error occured parsing the stream processor name: ${e}`) + new Error(`An error occurred parsing the stream processor name: ${e}`) ); } diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 11e79ccd8..8319bb77e 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -156,12 +156,14 @@ export default class MDBExtensionController implements vscode.Disposable { // Register our extension's commands. These are the event handlers and // control the functionality of our extension. // ------ CONNECTION ------ // - this.registerCommand(EXTENSION_COMMANDS.MDB_OPEN_OVERVIEW_PAGE, () => - this._webviewController.openWebview(this._context) - ); - this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT, () => - this._webviewController.openWebview(this._context) - ); + this.registerCommand(EXTENSION_COMMANDS.MDB_OPEN_OVERVIEW_PAGE, () => { + this._webviewController.openWebview(this._context); + return Promise.resolve(true); + }); + this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT, () => { + this._webviewController.openWebview(this._context); + return Promise.resolve(true); + }); this.registerCommand(EXTENSION_COMMANDS.MDB_CONNECT_WITH_URI, () => this._connectionController.connectWithURI() ); @@ -288,18 +290,22 @@ export default class MDBExtensionController implements vscode.Disposable { } registerTreeViewCommands(): void { - this.registerCommand(EXTENSION_COMMANDS.MDB_ADD_CONNECTION, () => - this._webviewController.openWebview(this._context) - ); + this.registerCommand(EXTENSION_COMMANDS.MDB_ADD_CONNECTION, () => { + this._webviewController.openWebview(this._context); + return Promise.resolve(true); + }); this.registerCommand(EXTENSION_COMMANDS.MDB_ADD_CONNECTION_WITH_URI, () => this._connectionController.connectWithURI() ); this.registerCommand( EXTENSION_COMMANDS.MDB_CONNECT_TO_CONNECTION_TREE_VIEW, - (connectionTreeItem: ConnectionTreeItem) => - this._connectionController.connectWithConnectionId( - connectionTreeItem.connectionId - ) + async (connectionTreeItem: ConnectionTreeItem) => { + const { successfullyConnected } = + await this._connectionController.connectWithConnectionId( + connectionTreeItem.connectionId + ); + return successfullyConnected; + } ); this.registerCommand( EXTENSION_COMMANDS.MDB_DISCONNECT_FROM_CONNECTION_TREE_VIEW, @@ -343,6 +349,31 @@ export default class MDBExtensionController implements vscode.Disposable { (element: ConnectionTreeItem) => this._connectionController.removeMongoDBConnection(element.connectionId) ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_EDIT_CONNECTION, + (element: ConnectionTreeItem) => { + const connectionOptions = + this._connectionController.getConnectionConnectionOptions( + element.connectionId + ); + + if (!connectionOptions) { + return Promise.resolve(false); + } + + void this._webviewController.openEditConnection({ + connection: { + id: element.connectionId, + name: this._connectionController.getSavedConnectionName( + element.connectionId + ), + connectionOptions, + }, + context: this._context, + }); + return Promise.resolve(true); + } + ); this.registerCommand( EXTENSION_COMMANDS.MDB_RENAME_CONNECTION, (element: ConnectionTreeItem) => diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index 052e88fd3..56bb2f166 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { - getConnectionTitle, extractSecrets, + getConnectionTitle, mergeSecrets, } from '@mongodb-js/connection-info'; import type { ConnectionOptions } from 'mongodb-data-service'; @@ -11,7 +11,6 @@ import type StorageController from './storageController'; import type { SecretStorageLocationType } from './storageController'; import { DefaultSavingLocations, - SecretStorageLocation, StorageLocation, StorageVariables, } from './storageController'; @@ -42,23 +41,24 @@ export class ConnectionStorage { this._storageController = storageController; } - // Returns the saved connection (without secrets). - async saveNewConnection(connection: { + createNewConnection({ + connectionOptions, + connectionId, + }: { connectionOptions: ConnectionOptions; - id: string; - }): Promise { - const name = getConnectionTitle(connection); - const newConnectionInfo = { - id: connection.id, + connectionId: string; + }): LoadedConnection { + const name = getConnectionTitle({ + connectionOptions, + }); + + return { + id: connectionId, name, - // To begin we just store it on the session, the storage controller - // handles changing this based on user preference. - storageLocation: StorageLocation.NONE, - secretStorageLocation: SecretStorageLocation.SecretStorage, - connectionOptions: connection.connectionOptions, + storageLocation: this.getPreferredStorageLocationFromConfiguration(), + secretStorageLocation: 'vscode.SecretStorage', + connectionOptions: connectionOptions, }; - - return await this.saveConnectionWithSecrets(newConnectionInfo); } async _getConnectionInfoWithSecrets( @@ -91,34 +91,6 @@ export class ConnectionStorage { } } - async saveConnection( - storeConnectionInfo: T - ): Promise { - const dontShowSaveLocationPrompt = vscode.workspace - .getConfiguration('mdb.connectionSaving') - .get('hideOptionToChooseWhereToSaveNewConnections'); - - if (dontShowSaveLocationPrompt === true) { - // The user has chosen not to show the message on where to save the connection. - // Save the connection in their default preference. - storeConnectionInfo.storageLocation = - this.getPreferredStorageLocationFromConfiguration(); - } else { - storeConnectionInfo.storageLocation = - await this.getStorageLocationFromPrompt(); - } - - if ( - [StorageLocation.GLOBAL, StorageLocation.WORKSPACE].includes( - storeConnectionInfo.storageLocation - ) - ) { - await this._saveConnectionToStore(storeConnectionInfo); - } - - return storeConnectionInfo; - } - _mergedConnectionInfoWithSecrets( connectionInfo: LoadedConnection, unparsedSecrets: string @@ -142,37 +114,40 @@ export class ConnectionStorage { }; } - private async saveConnectionWithSecrets( - newStoreConnectionInfoWithSecrets: LoadedConnection - ): Promise { + async saveConnection(connection: LoadedConnection): Promise { + if ( + ![StorageLocation.GLOBAL, StorageLocation.WORKSPACE].includes( + connection.storageLocation + ) + ) { + return; + } // We don't want to store secrets to disc. - const { connectionInfo: safeConnectionInfo, secrets } = extractSecrets( - newStoreConnectionInfoWithSecrets - ); - const savedConnectionInfo = await this.saveConnection({ - ...newStoreConnectionInfoWithSecrets, + const { connectionInfo: safeConnectionInfo, secrets } = + extractSecrets(connection); + await this._saveConnectionToStore({ + ...connection, connectionOptions: safeConnectionInfo.connectionOptions, // The connection info without secrets. }); + await this._storageController.setSecret( - savedConnectionInfo.id, + connection.id, JSON.stringify(secrets) ); - - return savedConnectionInfo; } async _saveConnectionToStore( - storeConnectionInfo: StoreConnectionInfo + connectionWithoutSecrets: StoreConnectionInfo ): Promise { const variableName = - storeConnectionInfo.storageLocation === StorageLocation.GLOBAL + connectionWithoutSecrets.storageLocation === StorageLocation.GLOBAL ? StorageVariables.GLOBAL_SAVED_CONNECTIONS : StorageVariables.WORKSPACE_SAVED_CONNECTIONS; // Get the current saved connections. let savedConnections = this._storageController.get( variableName, - storeConnectionInfo.storageLocation + connectionWithoutSecrets.storageLocation ); if (!savedConnections) { @@ -180,41 +155,14 @@ export class ConnectionStorage { } // Add the new connection. - savedConnections[storeConnectionInfo.id] = storeConnectionInfo; + savedConnections[connectionWithoutSecrets.id] = connectionWithoutSecrets; // Update the store. return this._storageController.update( variableName, savedConnections, - storeConnectionInfo.storageLocation - ); - } - - async getStorageLocationFromPrompt() { - const storeOnWorkspace = 'Save the connection on this workspace'; - const storeGlobally = 'Save the connection globally on vscode'; - // Prompt the user where they want to save the new connection. - const chosenConnectionSavingLocation = await vscode.window.showQuickPick( - [ - storeOnWorkspace, - storeGlobally, - "Don't save this connection (it will be lost when the session is closed)", - ], - { - placeHolder: - 'Where would you like to save this new connection? (This message can be disabled in the extension settings.)', - } + connectionWithoutSecrets.storageLocation ); - - if (chosenConnectionSavingLocation === storeOnWorkspace) { - return StorageLocation.WORKSPACE; - } - - if (chosenConnectionSavingLocation === storeGlobally) { - return StorageLocation.GLOBAL; - } - - return StorageLocation.NONE; } async loadConnections() { @@ -250,7 +198,7 @@ export class ConnectionStorage { await Promise.all( toBeReSaved.map(async (connectionInfo) => { - await this.saveConnectionWithSecrets(connectionInfo); + await this.saveConnection(connectionInfo); }) ); diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 94f62688e..cd95874f0 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -74,6 +74,10 @@ type KeytarSecretsMigrationFailedProperties = { connections_with_failed_keytar_migration: number; }; +type ConnectionEditedTelemetryEventProperties = { + success: boolean; +}; + type SavedConnectionsLoadedProperties = { // Total number of connections saved on disk saved_connections: number; @@ -92,6 +96,7 @@ export type TelemetryEventProperties = | ExtensionCommandRunTelemetryEventProperties | NewConnectionTelemetryEventProperties | DocumentUpdatedTelemetryEventProperties + | ConnectionEditedTelemetryEventProperties | DocumentEditedTelemetryEventProperties | QueryExportedTelemetryEventProperties | PlaygroundCreatedTelemetryEventProperties @@ -105,6 +110,8 @@ export enum TelemetryEventTypes { EXTENSION_LINK_CLICKED = 'Link Clicked', EXTENSION_COMMAND_RUN = 'Command Run', NEW_CONNECTION = 'New Connection', + CONNECTION_EDITED = 'Connection Edited', + OPEN_EDIT_CONNECTION = 'Open Edit Connection', PLAYGROUND_SAVED = 'Playground Saved', PLAYGROUND_LOADED = 'Playground Loaded', DOCUMENT_UPDATED = 'Document Updated', diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 91efc89a2..7c9df1db2 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { afterEach, beforeEach } from 'mocha'; import assert from 'assert'; import * as mongodbDataService from 'mongodb-data-service'; +import { expect } from 'chai'; import ConnectionString from 'mongodb-connection-string-url'; import ConnectionController, { @@ -278,16 +279,14 @@ suite('Connection Controller Test Suite', function () { const connections = testConnectionController._connections; - assert.strictEqual(Object.keys(connections).length, 4); - assert.strictEqual( - connections[Object.keys(connections)[0]].name, + expect(Object.keys(connections).length).to.equal(4); + expect(connections[Object.keys(connections)[0]].name).to.equal( 'localhost:27088' ); - assert.strictEqual( + expect( connections[Object.keys(connections)[2]].connectionOptions - ?.connectionString, - expectedDriverUrl - ); + ?.connectionString + ).to.equal(expectedDriverUrl); }); test('when a connection is added it is saved to the global storage', async () => { @@ -365,10 +364,10 @@ suite('Connection Controller Test Suite', function () { }, }; - const successfulConnection = + const connectionResult = await testConnectionController.connectWithConnectionId('25'); - assert.strictEqual(successfulConnection, true); + assert.strictEqual(connectionResult.successfullyConnected, true); assert.strictEqual(testConnectionController.getActiveConnectionId(), '25'); }); @@ -638,6 +637,68 @@ suite('Connection Controller Test Suite', function () { assert.strictEqual(name, 'new connection name'); }); + test('a saved to workspace connection can be updated and loaded', async () => { + await testConnectionController.loadSavedConnections(); + await vscode.workspace + .getConfiguration('mdb.connectionSaving') + .update( + 'defaultConnectionSavingLocation', + DefaultSavingLocations.Workspace + ); + await testConnectionController.addNewConnectionStringAndConnect( + TEST_DATABASE_URI + ); + + const workspaceStoreConnections = testStorageController.get( + StorageVariables.WORKSPACE_SAVED_CONNECTIONS, + StorageLocation.WORKSPACE + ); + + assert.strictEqual(Object.keys(workspaceStoreConnections).length, 1); + + const connectionId = + testConnectionController.getActiveConnectionId() || 'zz'; + + const updatedConnectionString = new ConnectionString(TEST_DATABASE_URI); + updatedConnectionString.searchParams.set('connectTimeoutMS', '5000'); + const updateSuccess = + await testConnectionController.updateConnectionAndConnect({ + connectionId, + connectionOptions: { + connectionString: updatedConnectionString.toString(), + }, + }); + + assert.strictEqual(updateSuccess.successfullyConnected, true); + + await testConnectionController.disconnect(); + + testConnectionController.clearAllConnections(); + + assert.strictEqual( + testConnectionController.getSavedConnections().length, + 0 + ); + + // Activate (which will load the past connection). + await testConnectionController.loadSavedConnections(); + + assert.strictEqual( + testConnectionController.getSavedConnections().length, + 1 + ); + + const id = testConnectionController.getSavedConnections()[0].id; + const connectTimeoutMS = new ConnectionString( + testConnectionController.getSavedConnections()[0].connectionOptions.connectionString + ).searchParams.get('connectTimeoutMS'); + const name = testConnectionController._connections[id || 'x'].name; + + assert.strictEqual(name, 'localhost:27088'); + // Ensure it's updated. + assert.strictEqual(connectTimeoutMS, '5000'); + }); + test('close connection string input calls to cancel the cancellation token', function (done) { const inputBoxResolvesStub = sandbox.stub(); inputBoxResolvesStub.callsFake(() => { @@ -770,7 +831,7 @@ suite('Connection Controller Test Suite', function () { } catch (err) { assert( false, - `Expected not to error when disconnecting multiple times, recieved: ${err}` + `Expected not to error when disconnecting multiple times, received: ${err}` ); } @@ -888,43 +949,26 @@ suite('Connection Controller Test Suite', function () { StorageVariables.GLOBAL_SAVED_CONNECTIONS ); - assert.strictEqual( - !!workspaceStoreConnections, - true, - `Expected workspace store to have connections, found ${workspaceStoreConnections}` - ); - + expect(workspaceStoreConnections).to.not.be.empty; const connections = Object.values(workspaceStoreConnections); - assert.strictEqual(connections.length, 1); - assert.strictEqual( - connections[0].connectionOptions?.connectionString.includes( - TEST_USER_USERNAME - ), - true - ); - assert.strictEqual( - connections[0].connectionOptions?.connectionString.includes( - TEST_USER_PASSWORD - ), - false - ); - assert.strictEqual( - connections[0].connectionOptions?.connectionString.includes( - `appname=mongodb-vscode+${version}` - ), - true + expect(connections.length).to.equal(1); + expect(connections[0].connectionOptions?.connectionString).to.include( + TEST_USER_USERNAME ); - assert.strictEqual( - testConnectionController._connections[ - connections[0].id - ].connectionOptions?.connectionString.includes(TEST_USER_PASSWORD), - true + expect(connections[0].connectionOptions?.connectionString).to.not.include( + TEST_USER_PASSWORD ); - assert.strictEqual( - testConnectionController._connections[connections[0].id].name, - 'localhost:27088' + expect(connections[0].connectionOptions?.connectionString).to.include( + `appname=mongodb-vscode+${version}` ); + expect( + testConnectionController._connections[connections[0].id].connectionOptions + ?.connectionString + ).to.include(TEST_USER_PASSWORD); + expect( + testConnectionController._connections[connections[0].id].name + ).to.equal('localhost:27088'); }); test('getMongoClientConnectionOptions returns url and options properties', async () => { diff --git a/src/test/suite/explorer/databaseTreeItem.test.ts b/src/test/suite/explorer/databaseTreeItem.test.ts index 3c382fa8f..d940316b6 100644 --- a/src/test/suite/explorer/databaseTreeItem.test.ts +++ b/src/test/suite/explorer/databaseTreeItem.test.ts @@ -56,7 +56,7 @@ suite('DatabaseTreeItem Test Suite', () => { assert.strictEqual( collections.length, 0, - `Expected no collections to be returned, recieved ${collections.length}` + `Expected no collections to be returned, received ${collections.length}` ); }); @@ -68,7 +68,7 @@ suite('DatabaseTreeItem Test Suite', () => { const collections = await testDatabaseTreeItem.getChildren(); assert( collections.length > 0, - `Expected more than one collection to be returned, recieved ${collections.length}` + `Expected more than one collection to be returned, received ${collections.length}` ); assert.strictEqual( diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 51c54db95..96f0d8d1a 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -743,7 +743,7 @@ suite('MDBExtensionController Test Suite', function () { const expectedMessage = 'Drop collection failed: ns not found'; assert( showErrorMessageStub.firstCall.args[0] === expectedMessage, - `Expected "${expectedMessage}" when dropping a collection that doesn't exist, recieved "${showErrorMessageStub.firstCall.args[0]}"` + `Expected "${expectedMessage}" when dropping a collection that doesn't exist, received "${showErrorMessageStub.firstCall.args[0]}"` ); await testConnectionController.disconnect(); testConnectionController.clearAllConnections(); diff --git a/src/test/suite/oidc.test.ts b/src/test/suite/oidc.test.ts index fc6a1450c..2fef7f337 100644 --- a/src/test/suite/oidc.test.ts +++ b/src/test/suite/oidc.test.ts @@ -235,7 +235,8 @@ suite('OIDC Tests', function () { await testConnectionController.disconnect(); expect( - await testConnectionController.connectWithConnectionId(connectionId) + (await testConnectionController.connectWithConnectionId(connectionId)) + .successfullyConnected ).to.be.true; expect(tokenFetchCalls).to.equal(1); }); @@ -264,7 +265,8 @@ suite('OIDC Tests', function () { await testConnectionController.disconnect(); expect( - await testConnectionController.connectWithConnectionId(connectionId) + (await testConnectionController.connectWithConnectionId(connectionId)) + .successfullyConnected ).to.be.true; expect(tokenFetchCalls).to.equal(2); }); diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 8c80ea84e..30ae15719 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -20,12 +20,13 @@ import { ConnectionStorage } from '../../../storage/connectionStorage'; const testDatabaseConnectionName = 'localhost:27088'; -const newTestConnection = (id: string) => ({ - id, - connectionOptions: { - connectionString: TEST_DATABASE_URI, - }, -}); +const newTestConnection = (connectionStorage: ConnectionStorage, id: string) => + connectionStorage.createNewConnection({ + connectionId: id, + connectionOptions: { + connectionString: TEST_DATABASE_URI, + }, + }); suite('Connection Storage Test Suite', function () { const extensionContextStub = new ExtensionContextStub(); @@ -64,8 +65,12 @@ suite('Connection Storage Test Suite', function () { .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionStorage.saveNewConnection(newTestConnection('1')); - await testConnectionStorage.saveNewConnection(newTestConnection('2')); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '1') + ); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '2') + ); await vscode.workspace .getConfiguration('mdb.connectionSaving') .update( @@ -73,8 +78,12 @@ suite('Connection Storage Test Suite', function () { DefaultSavingLocations.Workspace ); - await testConnectionStorage.saveNewConnection(newTestConnection('3')); - await testConnectionStorage.saveNewConnection(newTestConnection('4')); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '3') + ); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '4') + ); const connections = await testConnectionStorage.loadConnections(); @@ -94,7 +103,9 @@ suite('Connection Storage Test Suite', function () { .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); - await testConnectionStorage.saveNewConnection(newTestConnection('1')); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '1') + ); const globalStoreConnections = testStorageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS, @@ -123,7 +134,9 @@ suite('Connection Storage Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations.Workspace ); - await testConnectionStorage.saveNewConnection(newTestConnection('1')); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '1') + ); const workspaceStoreConnections = testStorageController.get( StorageVariables.WORKSPACE_SAVED_CONNECTIONS, @@ -154,7 +167,9 @@ suite('Connection Storage Test Suite', function () { 'defaultConnectionSavingLocation', DefaultSavingLocations['Session Only'] ); - await testConnectionStorage.saveNewConnection(newTestConnection('1')); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '1') + ); const objectString = JSON.stringify(undefined); const globalStoreConnections = testStorageController.get( @@ -180,8 +195,8 @@ suite('Connection Storage Test Suite', function () { DefaultSavingLocations.Workspace ); const connectionId = 'pie'; - await testConnectionStorage.saveNewConnection( - newTestConnection(connectionId) + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, connectionId) ); const workspaceStoreConnections = testStorageController.get( @@ -205,8 +220,8 @@ suite('Connection Storage Test Suite', function () { .getConfiguration('mdb.connectionSaving') .update('defaultConnectionSavingLocation', DefaultSavingLocations.Global); const connectionId = 'pineapple'; - await testConnectionStorage.saveNewConnection( - newTestConnection(connectionId) + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, connectionId) ); const globalStoreConnections = testStorageController.get( @@ -233,8 +248,8 @@ suite('Connection Storage Test Suite', function () { ); const connectionId = 'pie'; - await testConnectionStorage.saveNewConnection( - newTestConnection(connectionId) + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, connectionId) ); await testConnectionStorage.removeConnection(connectionId); @@ -312,13 +327,17 @@ suite('Connection Storage Test Suite', function () { }); test('should be able to load connection with its secrets', async () => { - await testConnectionStorage.saveNewConnection(newTestConnection('1')); - await testConnectionStorage.saveNewConnection({ - id: '2', - connectionOptions: { - connectionString: TEST_DATABASE_URI_USER, - }, - }); + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, '1') + ); + await testConnectionStorage.saveConnection( + testConnectionStorage.createNewConnection({ + connectionId: '2', + connectionOptions: { + connectionString: TEST_DATABASE_URI_USER, + }, + }) + ); // By default the connection secrets are already stored in SecretStorage const savedConnections = await testConnectionStorage.loadConnections(); @@ -405,16 +424,16 @@ suite('Connection Storage Test Suite', function () { DefaultSavingLocations.Workspace ); - await testConnectionStorage.saveNewConnection( - newTestConnection('pineapple') + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, 'pineapple') ); expect(testConnectionStorage.hasSavedConnections()).to.equal(true); }); test('when there are saved global connections, hasSavedConnections returns true', async () => { - await testConnectionStorage.saveNewConnection( - newTestConnection('pineapple') + await testConnectionStorage.saveConnection( + newTestConnection(testConnectionStorage, 'pineapple') ); expect(testConnectionStorage.hasSavedConnections()).to.equal(true); diff --git a/src/test/suite/views/webview-app/overview-page.test.tsx b/src/test/suite/views/webview-app/overview-page.test.tsx index 84addf5e7..43b9cafbd 100644 --- a/src/test/suite/views/webview-app/overview-page.test.tsx +++ b/src/test/suite/views/webview-app/overview-page.test.tsx @@ -76,15 +76,15 @@ describe('OverviewPage test suite', function () { const postMessageSpy = Sinon.spy(vscode, 'postMessage'); screen.getByText('Open form').click(); screen.getByTestId('connect-button').click(); - const connectionAttemptId = (postMessageSpy.lastCall.args[0] as any) - .connectionAttemptId; + const connectionId = (postMessageSpy.lastCall.args[0] as any) + .connectionInfo.id; act(() => { window.dispatchEvent( new MessageEvent('message', { data: { command: MESSAGE_TYPES.CONNECT_RESULT, - connectionAttemptId, + connectionId, connectionSuccess: false, connectionMessage: 'server not found', }, @@ -99,15 +99,15 @@ describe('OverviewPage test suite', function () { const postMessageSpy = Sinon.spy(vscode, 'postMessage'); screen.getByText('Open form').click(); screen.getByTestId('connect-button').click(); - const connectionAttemptId = (postMessageSpy.lastCall.args[0] as any) - .connectionAttemptId; + const connectionId = (postMessageSpy.lastCall.args[0] as any) + .connectionInfo.id; act(() => { window.dispatchEvent( new MessageEvent('message', { data: { command: MESSAGE_TYPES.CONNECT_RESULT, - connectionAttemptId, + connectionId, connectionSuccess: true, connectionMessage: '', }, @@ -117,6 +117,50 @@ describe('OverviewPage test suite', function () { expect(screen.queryByTestId(connectionFormTestId)).to.not.exist; }); + it('should handle editing a connection', function () { + render(); + + const postMessageSpy = Sinon.spy(vscode, 'postMessage'); + expect(screen.queryByTestId(connectionFormTestId)).to.not.exist; + expect(screen.queryByText('pineapple')).to.not.exist; + + act(() => { + window.dispatchEvent( + new MessageEvent('message', { + data: { + command: MESSAGE_TYPES.OPEN_EDIT_CONNECTION, + connection: { + id: 'pear', + name: 'pineapple', + connectionOptions: { + connectionString: 'mongodb://localhost:27099', + }, + }, + }, + }) + ); + }); + + // Shows the connection name that's being edited.. + expect(screen.getByTestId(connectionFormTestId)).to.exist; + expect(screen.getByText('pineapple')).to.exist; + + expect(postMessageSpy).to.not.be.called; + screen.getByTestId('connect-button').click(); + expect(postMessageSpy).to.be.calledOnce; + + const editAttempt = postMessageSpy.lastCall.args[0] as any; + expect(editAttempt).to.deep.equal({ + command: 'EDIT_AND_CONNECT_CONNECTION', + connectionInfo: { + id: 'pear', + connectionOptions: { + connectionString: 'mongodb://localhost:27099', + }, + }, + }); + }); + it('should not display results from other connection attempts', function () { render(); screen.getByText('Open form').click(); @@ -127,7 +171,7 @@ describe('OverviewPage test suite', function () { new MessageEvent('message', { data: { command: MESSAGE_TYPES.CONNECT_RESULT, - connectionAttemptId: 1, // different from the attempt id generated by our click + connectionId: 1, // different from the attempt id generated by our click connectionSuccess: true, connectionMessage: '', }, @@ -142,7 +186,7 @@ describe('OverviewPage test suite', function () { new MessageEvent('message', { data: { command: MESSAGE_TYPES.CONNECT_RESULT, - connectionAttemptId: 2, // different from the attempt id generated by our click + connectionId: 2, // different from the attempt id generated by our click connectionSuccess: false, connectionMessage: 'something bad happened', }, diff --git a/src/test/suite/views/webviewController.test.ts b/src/test/suite/views/webviewController.test.ts index 040fc76f8..b2b340612 100644 --- a/src/test/suite/views/webviewController.test.ts +++ b/src/test/suite/views/webviewController.test.ts @@ -20,8 +20,29 @@ import * as linkHelper from '../../../utils/linkHelper'; suite('Webview Test Suite', () => { const sandbox = sinon.createSandbox(); + let extensionContextStub: ExtensionContextStub; + let testStorageController: StorageController; + let testTelemetryService: TelemetryService; + let testConnectionController: ConnectionController; + let testWebviewController: WebviewController; beforeEach(() => { + extensionContextStub = new ExtensionContextStub(); + testStorageController = new StorageController(extensionContextStub); + testTelemetryService = new TelemetryService( + testStorageController, + extensionContextStub + ); + testConnectionController = new ConnectionController({ + statusView: new StatusView(extensionContextStub), + storageController: testStorageController, + telemetryService: testTelemetryService, + }); + testWebviewController = new WebviewController({ + connectionController: testConnectionController, + storageController: testStorageController, + telemetryService: testTelemetryService, + }); sandbox.stub( mdbTestExtension.testExtensionController._telemetryService, 'trackNewConnection' @@ -188,17 +209,6 @@ suite('Webview Test Suite', () => { }); test('web view listens for a connect message and adds the connection', (done) => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceivedSet = false; let messageReceived; @@ -234,12 +244,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -258,22 +262,10 @@ suite('Webview Test Suite', () => { connectionString: 'mongodb://localhost:27088', }, }, - connectionAttemptId: 1, }); }); test('web view sends a successful connect result on a successful connection', (done) => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceivedSet = false; let messageReceived; @@ -309,12 +301,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -327,9 +313,8 @@ suite('Webview Test Suite', () => { // Mock a connection call. messageReceived({ command: MESSAGE_TYPES.CONNECT, - connectionAttemptId: 'pineapple', connectionInfo: { - id: 'test', + id: 'pineapple', connectionOptions: { connectionString: 'mongodb://localhost:27088', }, @@ -338,17 +323,6 @@ suite('Webview Test Suite', () => { }); test('web view sends an unsuccessful connect result on an unsuccessful connection', (done) => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceived; sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -378,12 +352,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -391,9 +359,8 @@ suite('Webview Test Suite', () => { // Mock a connection call. messageReceived({ command: MESSAGE_TYPES.CONNECT, - connectionAttemptId: 'pineapple', connectionInfo: { - id: 'test', + id: 'pineapple', connectionOptions: { // bad port number. connectionString: 'mongodb://localhost:2700002', @@ -405,17 +372,6 @@ suite('Webview Test Suite', () => { test('web view sends an unsuccessful connect result on an attempt that is overridden', function (done) { this.timeout(5000); - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceived; sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -450,11 +406,6 @@ suite('Webview Test Suite', () => { 'createWebviewPanel', fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -462,9 +413,8 @@ suite('Webview Test Suite', () => { // Mock a connection call. messageReceived({ command: MESSAGE_TYPES.CONNECT, - connectionAttemptId: 'pineapple', connectionInfo: { - id: 'test', + id: 'pineapple', connectionOptions: { connectionString: 'mongodb://shouldfail:27088?connectTimeoutMS=500&serverSelectionTimeoutMS=500&socketTimeoutMS=500', @@ -477,18 +427,7 @@ suite('Webview Test Suite', () => { ); }); - test('web view runs the "connectWithURI" command when open connection string input is recieved', (done) => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); + test('web view runs the "connectWithURI" command when open connection string input is received', (done) => { let messageReceived; sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -519,12 +458,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -544,17 +477,6 @@ suite('Webview Test Suite', () => { }); test('webview returns the connection status on a connection status request', (done) => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceived; sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -584,12 +506,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -601,17 +517,6 @@ suite('Webview Test Suite', () => { }); test('webview returns the connection status on a connection status request', (done) => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceived; sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -642,12 +547,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -663,17 +562,6 @@ suite('Webview Test Suite', () => { }); test('calls to rename the active connection when a rename active connection message is passed', async () => { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, - }); let messageReceived; sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -706,12 +594,6 @@ suite('Webview Test Suite', () => { mockRenameConnectionOnConnectionController ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); @@ -734,19 +616,61 @@ suite('Webview Test Suite', () => { await testConnectionController.disconnect(); }); - test('it notifies all the webviews of the change of current theme and gulps the error if any', function (done) { - const extensionContextStub = new ExtensionContextStub(); - const testStorageController = new StorageController(extensionContextStub); - const testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub + test('calls to edit a connection when an edit connection message is passed', async () => { + sandbox.stub(testTelemetryService, 'trackNewConnection'); + + let messageReceived; + sandbox.stub(vscode.window, 'createWebviewPanel').returns({ + webview: { + html: '', + postMessage: (): void => {}, + onDidReceiveMessage: (callback): void => { + messageReceived = callback; + }, + asWebviewUri: sandbox.fake.returns(''), + }, + onDidDispose: sandbox.fake.returns(''), + } as unknown as vscode.WebviewPanel); + + const mockEditConnectionOnConnectionController = sandbox + .stub(testConnectionController, 'updateConnectionAndConnect') + .returns( + Promise.resolve({ + successfullyConnected: true, + connectionErrorMessage: '', + }) + ); + + void testWebviewController.openWebview( + mdbTestExtension.extensionContextStub ); - const testConnectionController = new ConnectionController({ - statusView: new StatusView(extensionContextStub), - storageController: testStorageController, - telemetryService: testTelemetryService, + + // Mock a connection status request call. + messageReceived({ + command: MESSAGE_TYPES.EDIT_AND_CONNECT_CONNECTION, + connectionInfo: { + id: 'pineapple', + connectionOptions: { + connectionString: 'test', + }, + }, }); + assert(mockEditConnectionOnConnectionController.called); + assert.deepStrictEqual( + mockEditConnectionOnConnectionController.firstCall.args[0], + { + connectionId: 'pineapple', + connectionOptions: { + connectionString: 'test', + }, + } + ); + + await testConnectionController.disconnect(); + }); + + test('it notifies all the webviews of the change of current theme and gulps the error if any', function (done) { sandbox.stub(testTelemetryService, 'trackNewConnection'); const totalExpectedPostMessageCalls = 3; @@ -779,12 +703,6 @@ suite('Webview Test Suite', () => { fakeVSCodeCreateWebviewPanel ); - const testWebviewController = new WebviewController({ - connectionController: testConnectionController, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - void testWebviewController.openWebview( mdbTestExtension.extensionContextStub ); diff --git a/src/views/webview-app/connection-form.tsx b/src/views/webview-app/connection-form.tsx index f13d329ed..e917063b4 100644 --- a/src/views/webview-app/connection-form.tsx +++ b/src/views/webview-app/connection-form.tsx @@ -9,7 +9,6 @@ import { spacing, useDarkMode, } from '@mongodb-js/compass-components'; -import { v4 as uuidv4 } from 'uuid'; import { VSCODE_EXTENSION_OIDC_DEVICE_AUTH_ID } from './extension-app-message-constants'; const modalContentStyles = css({ @@ -42,32 +41,25 @@ const connectingContainerDarkModeStyles = css({ background: 'rgba(0, 0, 0, 0.8)', }); -function createNewConnectionInfo() { - return { - id: uuidv4(), - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - }; -} - -const initialConnectionInfo = createNewConnectionInfo(); - -const ConnectionForm: React.FunctionComponent<{ - isConnecting: boolean; - onCancelConnectClicked: () => void; - onConnectClicked: ComponentProps< - typeof CompassConnectionForm - >['onConnectClicked']; - onClose: () => void; - open: boolean; - connectionErrorMessage: string; -}> = ({ +const ConnectionForm: React.FunctionComponent< + { + isConnecting: boolean; + onCancelConnectClicked: () => void; + onClose: () => void; + open: boolean; + connectionErrorMessage: string; + } & Pick< + ComponentProps, + 'onConnectClicked' | 'onSaveConnectionClicked' | 'initialConnectionInfo' + > +> = ({ + initialConnectionInfo, connectionErrorMessage, isConnecting, onCancelConnectClicked, onConnectClicked, onClose, + onSaveConnectionClicked, open, }) => { const darkMode = useDarkMode(); @@ -99,8 +91,10 @@ const ConnectionForm: React.FunctionComponent<{
{ const [showResourcesPanel, setShowResourcesPanel] = useState(false); const { + initialConnectionInfo, isConnecting, - connectionFormOpened, + isConnectionFormOpen, openConnectionForm, closeConnectionForm, connectionErrorMessage, handleCancelConnectClicked, + handleSaveConnectionClicked, handleConnectClicked, } = useConnectionForm(); const handleResourcesPanelClose = useCallback( @@ -58,9 +60,16 @@ const OverviewPage: React.FC = () => { {showResourcesPanel && ( )} - {connectionFormOpened && ( + {isConnectionFormOpen && ( + handleSaveConnectionClicked({ + id, + connectionOptions, + }) + } onCancelConnectClicked={handleCancelConnectClicked} onConnectClicked={({ id, connectionOptions }) => handleConnectClicked({ @@ -69,7 +78,7 @@ const OverviewPage: React.FC = () => { }) } onClose={closeConnectionForm} - open={connectionFormOpened} + open={isConnectionFormOpen} connectionErrorMessage={connectionErrorMessage} /> )} diff --git a/src/views/webview-app/use-connection-form.ts b/src/views/webview-app/use-connection-form.ts index 1f7c39613..cb150e879 100644 --- a/src/views/webview-app/use-connection-form.ts +++ b/src/views/webview-app/use-connection-form.ts @@ -1,65 +1,214 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useReducer } from 'react'; import { v4 as uuidv4 } from 'uuid'; import type { ConnectionOptions } from 'mongodb-data-service'; + import { sendConnectToExtension, sendCancelConnectToExtension, sendFormOpenedToExtension, + sendEditConnectionToExtension, } from './vscode-api'; import { MESSAGE_TYPES } from './extension-app-message-constants'; import type { MESSAGE_FROM_EXTENSION_TO_WEBVIEW } from './extension-app-message-constants'; +type ConnectionInfo = { + id: string; + connectionOptions: ConnectionOptions; + favorite?: { + name: string; + }; +}; + +function createNewConnectionInfo(): ConnectionInfo { + return { + id: uuidv4(), + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + }; +} + +type State = { + initialConnectionInfo: ConnectionInfo; + isConnecting: boolean; + isConnectionFormOpen: boolean; + isEditingConnection: boolean; + connectionErrorMessage: string; +}; + +export function getDefaultConnectionFormState(): State { + return { + initialConnectionInfo: createNewConnectionInfo(), + isConnecting: false, + isConnectionFormOpen: false, + isEditingConnection: false, + connectionErrorMessage: '', + }; +} + +type Action = + | { + type: 'open-connection-form'; + } + | { + type: 'close-connection-form'; + } + | { + type: 'connection-result'; + connectionSuccess: boolean; + connectionMessage: string; + } + | { + type: 'open-edit-connection'; + connectionInfo: { + id: string; + favorite: { + name: string; + }; + connectionOptions: ConnectionOptions; + }; + } + | { + type: 'attempt-connect'; + }; + +function connectionFormReducer(state: State, action: Action): State { + switch (action.type) { + case 'open-connection-form': + return { + ...state, + isConnectionFormOpen: true, + isConnecting: false, + isEditingConnection: false, + connectionErrorMessage: '', + initialConnectionInfo: createNewConnectionInfo(), + }; + case 'close-connection-form': + return { + ...state, + isConnectionFormOpen: false, + }; + case 'connection-result': + return { + ...state, + isConnecting: false, + connectionErrorMessage: action.connectionMessage, + isConnectionFormOpen: !action.connectionSuccess, + }; + case 'open-edit-connection': + return { + ...state, + isConnectionFormOpen: true, + isConnecting: false, + isEditingConnection: true, + connectionErrorMessage: '', + initialConnectionInfo: action.connectionInfo, + }; + case 'attempt-connect': + return { + ...state, + // Clear the error message from previous connect attempt. + connectionErrorMessage: '', + isConnecting: true, + }; + default: + return state; + } +} + export default function useConnectionForm() { - const [isConnecting, setIsConnecting] = useState(false); - const [connectionFormOpened, setConnectionFormOpened] = useState(false); - const [connectionAttemptId, setConnectionAttemptId] = useState(''); - const [connectionErrorMessage, setConnectionErrorMessage] = useState(''); + const [ + { + initialConnectionInfo, + isConnecting, + isConnectionFormOpen, + isEditingConnection, + connectionErrorMessage, + }, + dispatch, + ] = useReducer(connectionFormReducer, { + ...getDefaultConnectionFormState(), + }); useEffect(() => { const handleConnectResultResponse = (event) => { const message: MESSAGE_FROM_EXTENSION_TO_WEBVIEW = event.data; if ( message.command === MESSAGE_TYPES.CONNECT_RESULT && - message.connectionAttemptId === connectionAttemptId + message.connectionId === initialConnectionInfo.id ) { - setIsConnecting(false); - if (message.connectionSuccess) { - setConnectionFormOpened(false); - } else { - setConnectionErrorMessage(message.connectionMessage); - } + dispatch({ + type: 'connection-result', + connectionSuccess: message.connectionSuccess, + connectionMessage: message.connectionMessage, + }); + } + }; + window.addEventListener('message', handleConnectResultResponse); + () => window.removeEventListener('message', handleConnectResultResponse); + }, [initialConnectionInfo]); + + useEffect(() => { + const handleConnectResultResponse = (event) => { + const message: MESSAGE_FROM_EXTENSION_TO_WEBVIEW = event.data; + if (message.command === MESSAGE_TYPES.OPEN_EDIT_CONNECTION) { + dispatch({ + type: 'open-edit-connection', + connectionInfo: { + id: message.connection.id, + favorite: { + name: message.connection.name, + }, + connectionOptions: message.connection.connectionOptions, + }, + }); } }; window.addEventListener('message', handleConnectResultResponse); () => window.removeEventListener('message', handleConnectResultResponse); - }, [connectionAttemptId]); + }, []); return { - connectionFormOpened, + isConnectionFormOpen, isConnecting, + initialConnectionInfo, connectionErrorMessage, openConnectionForm: () => { - setConnectionFormOpened(true); + dispatch({ + type: 'open-connection-form', + }); sendFormOpenedToExtension(); }, closeConnectionForm: () => { - setConnectionFormOpened(false); - setConnectionErrorMessage(''); + dispatch({ + type: 'close-connection-form', + }); }, handleCancelConnectClicked: () => { sendCancelConnectToExtension(); }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + handleSaveConnectionClicked: (connectionAttempt: { + id: string; + connectionOptions: ConnectionOptions; + }) => { + // no-op, this cannot be called as don't set the `showFavoriteActions` setting. + + return Promise.resolve(); + }, handleConnectClicked: (connectionAttempt: { id: string; connectionOptions: ConnectionOptions; }) => { - // Clears the error message from previous connect attempt - setConnectionErrorMessage(''); + dispatch({ + type: 'attempt-connect', + }); - const nextAttemptId = uuidv4(); - setConnectionAttemptId(nextAttemptId); - setIsConnecting(true); - sendConnectToExtension(connectionAttempt, nextAttemptId); + if (isEditingConnection) { + sendEditConnectionToExtension(connectionAttempt); + } else { + sendConnectToExtension(connectionAttempt); + } }, }; } diff --git a/src/views/webview-app/vscode-api.ts b/src/views/webview-app/vscode-api.ts index c52a3d66d..90585bf9f 100644 --- a/src/views/webview-app/vscode-api.ts +++ b/src/views/webview-app/vscode-api.ts @@ -11,14 +11,21 @@ interface VSCodeApi { declare const acquireVsCodeApi: () => VSCodeApi; const vscode = acquireVsCodeApi(); +export const sendEditConnectionToExtension = ( + connectionInfo: ConnectMessage['connectionInfo'] +) => { + vscode.postMessage({ + command: MESSAGE_TYPES.EDIT_AND_CONNECT_CONNECTION, + connectionInfo, + }); +}; + export const sendConnectToExtension = ( - connectionInfo: ConnectMessage['connectionInfo'], - connectionAttemptId: string + connectionInfo: ConnectMessage['connectionInfo'] ) => { vscode.postMessage({ command: MESSAGE_TYPES.CONNECT, connectionInfo, - connectionAttemptId, }); }; diff --git a/src/views/webviewController.ts b/src/views/webviewController.ts index e29a453df..3fe00de40 100644 --- a/src/views/webviewController.ts +++ b/src/views/webviewController.ts @@ -17,6 +17,7 @@ import { openLink } from '../utils/linkHelper'; import type { StorageController } from '../storage'; import type TelemetryService from '../telemetry/telemetryService'; import { getFeatureFlagsScript } from '../featureFlags'; +import { TelemetryEventTypes } from '../telemetry/telemetryService'; const log = createLogger('webview controller'); @@ -104,26 +105,36 @@ export default class WebviewController { this._themeChangedSubscription?.dispose(); } - handleWebviewConnectAttempt = async ( - panel: vscode.WebviewPanel, + handleWebviewConnectAttempt = async ({ + panel, + connection, + isEditingConnection, + }: { + panel: vscode.WebviewPanel; connection: { connectionOptions: ConnectionOptions; id: string; - }, - connectionAttemptId: string - ) => { + }; + isEditingConnection?: boolean; + }) => { try { const { successfullyConnected, connectionErrorMessage } = - await this._connectionController.saveNewConnectionAndConnect( - connection, - ConnectionTypes.CONNECTION_FORM - ); + isEditingConnection + ? await this._connectionController.updateConnectionAndConnect({ + connectionId: connection.id, + connectionOptions: connection.connectionOptions, + }) + : await this._connectionController.saveNewConnectionAndConnect({ + connectionId: connection.id, + connectionOptions: connection.connectionOptions, + connectionType: ConnectionTypes.CONNECTION_FORM, + }); try { // The webview may have been closed in which case this will throw. void panel.webview.postMessage({ command: MESSAGE_TYPES.CONNECT_RESULT, - connectionAttemptId, + connectionId: connection.id, connectionSuccess: successfullyConnected, connectionMessage: successfullyConnected ? `Successfully connected to ${this._connectionController.getActiveConnectionName()}.` @@ -139,7 +150,7 @@ export default class WebviewController { void panel.webview.postMessage({ command: MESSAGE_TYPES.CONNECT_RESULT, - connectionAttemptId, + connectionId: connection.id, connectionSuccess: false, connectionMessage: `Unable to load connection: ${error}`, }); @@ -153,15 +164,22 @@ export default class WebviewController { ): Promise => { switch (message.command) { case MESSAGE_TYPES.CONNECT: - await this.handleWebviewConnectAttempt( + await this.handleWebviewConnectAttempt({ panel, - message.connectionInfo, - message.connectionAttemptId - ); + connection: message.connectionInfo, + }); return; case MESSAGE_TYPES.CANCEL_CONNECT: this._connectionController.cancelConnectionAttempt(); return; + case MESSAGE_TYPES.EDIT_AND_CONNECT_CONNECTION: + await this.handleWebviewConnectAttempt({ + panel, + connection: message.connectionInfo, + isEditingConnection: true, + }); + this._telemetryService.track(TelemetryEventTypes.CONNECTION_EDITED); + return; case MESSAGE_TYPES.CREATE_NEW_PLAYGROUND: void vscode.commands.executeCommand( EXTENSION_COMMANDS.MDB_CREATE_PLAYGROUND_FROM_OVERVIEW_PAGE @@ -212,7 +230,7 @@ export default class WebviewController { } }; - onRecievedWebviewMessage = async ( + onReceivedWebviewMessage = async ( message: MESSAGE_FROM_WEBVIEW_TO_EXTENSION, panel: vscode.WebviewPanel ): Promise => { @@ -220,7 +238,7 @@ export default class WebviewController { try { await this.handleWebviewMessage(message, panel); } catch (err) { - log.error('Error occured when parsing message from webview', err); + log.error('Error occurred when parsing message from webview', err); return; } }; @@ -250,7 +268,30 @@ export default class WebviewController { } }; - openWebview(context: vscode.ExtensionContext): Promise { + openEditConnection = async ({ + connection, + context, + }: { + connection: { + id: string; + name?: string; + connectionOptions: ConnectionOptions; + }; + context: vscode.ExtensionContext; + }) => { + const webviewPanel = this.openWebview(context); + + // Wait for the panel to open. + await new Promise((resolve) => setTimeout(resolve, 200)); + this._telemetryService.track(TelemetryEventTypes.OPEN_EDIT_CONNECTION); + + void webviewPanel.webview.postMessage({ + command: MESSAGE_TYPES.OPEN_EDIT_CONNECTION, + connection, + }); + }; + + openWebview(context: vscode.ExtensionContext): vscode.WebviewPanel { log.info('Opening webview...'); const extensionPath = context.extensionPath; @@ -295,11 +336,11 @@ export default class WebviewController { // Handle messages from the webview. panel.webview.onDidReceiveMessage( (message: MESSAGE_FROM_WEBVIEW_TO_EXTENSION) => - this.onRecievedWebviewMessage(message, panel), + this.onReceivedWebviewMessage(message, panel), undefined, context.subscriptions ); - return Promise.resolve(true); + return panel; } }