diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 9b82c5040ac..4895f4deb6c 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -1,5 +1,5 @@ The following third-party software is used by and included in **Mongodb Compass**. -This document was automatically generated on Sun Dec 03 2023. +This document was automatically generated on Mon Dec 04 2023. ## List of dependencies @@ -257,7 +257,7 @@ This document was automatically generated on Sun Dec 03 2023. | **[ejson-shell-parser](#1d4694ba430009acb84cb34d7d2f60a89bdb1f8fc199574ccb0c34b6c7e389a7)** | 2.0.0 | MIT | | **[electron-dl](#e97e034c7b93c63e7a433d75f6f1de3e0668764225ebbd61dbde8d1b55d6f3b7)** | 3.5.0 | MIT | | **[electron-squirrel-startup](#dcda22e402581a033ec2a017d6d05c094bf3173c1b03ae0471b2ce9078d3f601)** | 1.0.0 | Apache-2.0 | -| **[electron](#8991646ea7c79546c0bfa223bb75305abc203fcf978a0aceda8e2a107148c482)** | 25.9.6 | MIT | +| **[electron](#ee3fe5f0c4ca78b4e9d08d00d743b14134520f1d87040a1f339bac471ca9bb29)** | 25.9.7 | MIT | | **[encodeurl](#b89152db475e86531e570f87b45d8a51aa5e5d87d4cc3b960cee7b8febf1d26a)** | 1.0.2 | MIT | | **[end-of-stream](#fadc10994f5fa767d06fb25cfff35fb17a895daf3bc3477c782907668ed16563)** | 1.4.4 | MIT | | **[ensure-error](#3b1eba5276d89414cef21a1007e85c4f1d6749bf57b300e082ab23975a41dbc9)** | 3.0.1 | MIT | @@ -513,7 +513,7 @@ This document was automatically generated on Sun Dec 03 2023. | **[react-is](#5746232ad830b635a6581ee7d3b826ee932c6877087c98cb46b94101eb5ce40b)** | 18.2.0 | MIT | | **[react-leaflet-draw](#d80b4b765d856cdefe411a073d3b3dde06100128005f1381b4d26d6cf53134c7)** | 0.19.0 | ISC | | **[react-leaflet](#a5fc1f0504a89a932a12c5a183b75a748207329f20af6078f926e182d55aee8d)** | 2.4.0 | MIT | -| **[react-redux](#7e0baaf577850a112812e76f9643d35df0f800dadaebe9095eb0cfcb21df687b)** | 8.0.5 | MIT | +| **[react-redux](#156f5c2e2cbbda376faae24e78cc75de697170101ff7e9d2955c0f891cffd6a8)** | 8.1.3 | MIT | | **[react-transition-group](#f8a526737bf3e6cc7928ce77b3fa8e6a880da418fd9363a0dae1122922f92b72)** | 4.4.5 | BSD-3-Clause | | **[react-virtualized-auto-sizer](#556ae2daaf1c576dcc4544e6bfe080cd68d0c6912265cf2ebe7bae81e75de55b)** | 1.0.6 | MIT | | **[react-window](#fcf3bd62a73691dc82efaf23f7667fb5dfe4ce1cb5e8740f3d53a3a85086ead2)** | 1.8.6 | MIT | @@ -521,7 +521,7 @@ This document was automatically generated on Sun Dec 03 2023. | **[readable-stream](#1a43c6d0d989d70bee45d514814640912b6da533d875c1c1cbfdf98138a9fbd7)** | 1.0.34 | MIT | | **[readable-stream](#8f2e1b78e9d8c62cbe33ca0c9055ab55b3025f7c3ac146f29c102adbdc187bf1)** | 2.3.7 | MIT | | **[readable-stream](#75bd2243ec5ecc92b8d7e9a2e9a1aa142f20f6a5aad6dc0d923cdab997766174)** | 3.6.0 | MIT | -| **[redux-thunk](#ab4f6b36b54f8f5ac006ccc1592b924c9dfa527c68956a7f53f14ea65f3aaf8a)** | 2.4.1 | MIT | +| **[redux-thunk](#7eabcce4f7274e0c876829cb939804a9704770a9a60419d514c11e3e97c01623)** | 2.4.2 | MIT | | **[redux](#98b5d53f97fab4eea98fb5f423cad33400855b69ac662f1fdf55f0fb9e33f2ab)** | 4.2.1 | MIT | | **[reflux-core](#7af6ea33b0ed18717d672b44743ae53dcef843ae464690bb9e10eb1df048e9ea)** | 0.3.0 | BSD-3-Clause | | **[reflux-state-mixin](#b550b09e44c1263378a50688b4e60d7b4ea29394abcaf0c93aba1078ab93f973)** | 0.7.0 | ISC | @@ -21123,9 +21123,9 @@ License files: See the License for the specific language governing permissions and limitations under the License. - + -### [electron](https://www.npmjs.com/package/electron) (version 25.9.6) +### [electron](https://www.npmjs.com/package/electron) (version 25.9.7) License tags: MIT @@ -37981,9 +37981,9 @@ License files: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + -### [react-redux](https://www.npmjs.com/package/react-redux) (version 8.0.5) +### [react-redux](https://www.npmjs.com/package/react-redux) (version 8.1.3) License tags: MIT @@ -38289,9 +38289,9 @@ License files: IN THE SOFTWARE. """ - + -### [redux-thunk](https://www.npmjs.com/package/redux-thunk) (version 2.4.1) +### [redux-thunk](https://www.npmjs.com/package/redux-thunk) (version 2.4.2) License tags: MIT diff --git a/configs/webpack-config-compass/package.json b/configs/webpack-config-compass/package.json index 267a75f819b..aff6776725f 100644 --- a/configs/webpack-config-compass/package.json +++ b/configs/webpack-config-compass/package.json @@ -69,12 +69,12 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "babel-loader": "^8.2.5", "babel-plugin-istanbul": "^5.2.0", - "browserslist": "^4.22.1", + "browserslist": "^4.22.2", "chalk": "^4.1.2", "cli-progress": "^3.9.1", "core-js": "^3.17.3", "css-loader": "^4.3.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "html-webpack-plugin": "^5.3.2", "less-loader": "^10.0.1", "mini-css-extract-plugin": "^2.3.0", diff --git a/package-lock.json b/package-lock.json index 0f35f791d4b..1e3cb8991a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "configs/*", "scripts" ], + "dependencies": { + "electron": "^25.9.7" + }, "devDependencies": { "@babel/core": "7.16.0", "@babel/parser": "7.16.0", @@ -182,12 +185,12 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "babel-loader": "^8.2.5", "babel-plugin-istanbul": "^5.2.0", - "browserslist": "^4.22.1", + "browserslist": "^4.22.2", "chalk": "^4.1.2", "cli-progress": "^3.9.1", "core-js": "^3.17.3", "css-loader": "^4.3.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "html-webpack-plugin": "^5.3.2", "less-loader": "^10.0.1", "mini-css-extract-plugin": "^2.3.0", @@ -4074,9 +4077,9 @@ } }, "node_modules/@electron/rebuild": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.3.1.tgz", - "integrity": "sha512-bQDWw9rkEGYW+gzPNFCD+ugJ8LIFSu0pORJl5fmrT+H8qETOIPAe99Klzg0wGaZRu9JN+5qLzKG+PehRGOlzmQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.4.0.tgz", + "integrity": "sha512-fi+O1zgxSmZR1X8oSOHRgCWALSS56dGHJ2AXLx9Ua3wg/NmBaMI/jpu7moU6T8lk/XRLnsC9Ds/Jo4I+UCkHAA==", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", @@ -7764,10 +7767,6 @@ "resolved": "packages/compass-crud", "link": true }, - "node_modules/@mongodb-js/compass-database": { - "resolved": "packages/compass-database", - "link": true - }, "node_modules/@mongodb-js/compass-databases-collections": { "resolved": "packages/databases-collections", "link": true @@ -7812,10 +7811,6 @@ "resolved": "packages/compass-indexes", "link": true }, - "node_modules/@mongodb-js/compass-instance": { - "resolved": "packages/compass-instance", - "link": true - }, "node_modules/@mongodb-js/compass-logging": { "resolved": "packages/compass-logging", "link": true @@ -7876,6 +7871,10 @@ "resolved": "packages/compass-welcome", "link": true }, + "node_modules/@mongodb-js/compass-workspaces": { + "resolved": "packages/compass-workspaces", + "link": true + }, "node_modules/@mongodb-js/connection-form": { "resolved": "packages/connection-form", "link": true @@ -17466,9 +17465,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "funding": [ { "type": "opencollective", @@ -17484,9 +17483,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -17768,9 +17767,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001546", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz", - "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "funding": [ { "type": "opencollective", @@ -20904,9 +20903,9 @@ } }, "node_modules/electron": { - "version": "25.9.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.6.tgz", - "integrity": "sha512-vddWieRqlAFOTNZDwHwD8/3dRua8dpob6zZOUurO8y5JcFjA5/Kl2dbU6nwq/LI5BAS/6lg575xNZTJfOVwCmA==", + "version": "25.9.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.7.tgz", + "integrity": "sha512-8aejD8NricfzeoUflPPpDU7M6yEppQyASoZBT7qgiKnEy6+M3SgetZNEmkoHMGK/NNxP8GcO5lRuvCyegI1mig==", "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", @@ -21439,9 +21438,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.542", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.542.tgz", - "integrity": "sha512-6+cpa00G09N3sfh2joln4VUXHquWrOFx3FLZqiVQvl45+zS9DskDBTPvob+BhvFRmTBkyDSk0vvLMMRo/qc6mQ==" + "version": "1.4.601", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.601.tgz", + "integrity": "sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==" }, "node_modules/electron-window": { "version": "0.8.1", @@ -33273,9 +33272,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nopt": { "version": "6.0.0", @@ -37591,9 +37590,9 @@ } }, "node_modules/react-redux": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", - "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -37608,7 +37607,7 @@ "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0", "react-native": ">=0.59", - "redux": "^4" + "redux": "^4 || ^5.0.0-beta.0" }, "peerDependenciesMeta": { "@types/react": { @@ -38159,9 +38158,9 @@ } }, "node_modules/redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "peerDependencies": { "redux": "^4" } @@ -43420,7 +43419,7 @@ "@mongodb-js/devtools-connect": "^2.4.2", "@mongodb-js/oidc-plugin": "^0.3.0", "compass-preferences-model": "^2.15.6", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-ipc": "^3.2.4", "keytar": "^7.9.0", "node-fetch": "^2.6.7", @@ -44162,14 +44161,13 @@ "system-ca": "^1.0.2" }, "devDependencies": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@electron/remote": "^2.1.0", "@mongodb-js/atlas-service": "^0.10.1", "@mongodb-js/compass-aggregations": "^9.21.0", "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-collection": "^4.19.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-explain-plan": "^6.20.0", "@mongodb-js/compass-export-to-language": "^8.21.0", @@ -44178,7 +44176,6 @@ "@mongodb-js/compass-home": "^6.20.1", "@mongodb-js/compass-import-export": "^7.19.1", "@mongodb-js/compass-indexes": "^5.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-query-bar": "^8.20.0", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", @@ -44209,7 +44206,7 @@ "compass-preferences-model": "^2.15.6", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "electron-devtools-installer": "^3.2.0", "electron-dl": "^3.5.0", "electron-mocha": "^10.1.0", @@ -44404,6 +44401,7 @@ "version": "7.6.1", "license": "SSPL", "dependencies": { + "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", "hadron-app-registry": "^9.0.14", "mongodb-data-service": "^22.15.1", @@ -44429,6 +44427,7 @@ "xvfb-maybe": "^0.2.1" }, "peerDependencies": { + "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", "hadron-app-registry": "^9.0.14", "mongodb-data-service": "^22.15.1", @@ -44468,11 +44467,12 @@ "version": "4.19.1", "license": "SSPL", "dependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", - "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", - "hadron-app-registry": "^9.0.14" + "hadron-app-registry": "^9.0.14", + "mongodb-data-service": "^22.15.1" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.11", @@ -44494,7 +44494,6 @@ "eslint": "^7.25.0", "mocha": "^10.2.0", "mongodb-collection-model": "^5.15.1", - "mongodb-data-service": "^22.15.1", "mongodb-instance-model": "^12.15.1", "mongodb-ns": "^2.4.0", "numeral": "^2.0.6", @@ -44509,11 +44508,12 @@ "xvfb-maybe": "^0.2.1" }, "peerDependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", - "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", + "mongodb-data-service": "^22.15.1", "react": "^17.0.2" } }, @@ -44897,7 +44897,7 @@ "classnames": "^2.2.6", "depcheck": "^1.4.1", "ejson-shell-parser": "^2.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app": "^5.15.1", @@ -44934,6 +44934,7 @@ "packages/compass-database": { "name": "@mongodb-js/compass-database", "version": "3.19.1", + "extraneous": true, "license": "SSPL", "dependencies": { "@mongodb-js/compass-components": "^1.19.0", @@ -45036,7 +45037,7 @@ "packages/compass-e2e-tests": { "version": "1.16.2", "devDependencies": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@mongodb-js/compass-test-server": "^0.1.6", "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/oidc-mock-provider": "^0.4.1", @@ -45055,7 +45056,7 @@ "cross-spawn": "^7.0.3", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "fast-glob": "^3.2.7", "glob": "^10.2.5", @@ -45950,7 +45951,7 @@ "d3-flextree": "2.1.2", "d3-hierarchy": "^3.1.2", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "lodash": "^4.17.21", @@ -46161,7 +46162,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "mocha": "^10.2.0", "nyc": "^15.1.0", @@ -46347,11 +46348,9 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-find-in-page": "^4.19.1", "@mongodb-js/compass-import-export": "^7.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", "@mongodb-js/compass-schema-validation": "^6.20.0", @@ -46360,6 +46359,7 @@ "@mongodb-js/compass-shell": "^3.19.1", "@mongodb-js/compass-sidebar": "^5.19.1", "@mongodb-js/compass-welcome": "^0.18.1", + "@mongodb-js/compass-workspaces": "^0.1.0", "@mongodb-js/connection-storage": "^0.6.6", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", @@ -46399,11 +46399,9 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-find-in-page": "^4.19.1", "@mongodb-js/compass-import-export": "^7.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", "@mongodb-js/compass-schema-validation": "^6.20.0", @@ -46412,6 +46410,7 @@ "@mongodb-js/compass-shell": "^3.19.1", "@mongodb-js/compass-sidebar": "^5.19.1", "@mongodb-js/compass-welcome": "^0.18.1", + "@mongodb-js/compass-workspaces": "^0.1.0", "@mongodb-js/connection-storage": "^0.6.6", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", @@ -46431,7 +46430,7 @@ "@mongodb-js/compass-utils": "^0.5.5", "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-app-registry": "^9.0.14", "hadron-document": "^8.4.3", "mongodb-data-service": "^22.15.1" @@ -46489,7 +46488,7 @@ "@mongodb-js/compass-utils": "^0.5.5", "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-app-registry": "^9.0.14", "hadron-document": "^8.4.3", "mongodb-data-service": "^22.15.1", @@ -46618,7 +46617,7 @@ "chai": "^4.2.0", "depcheck": "^1.4.1", "ejson-shell-parser": "^2.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", @@ -46720,6 +46719,7 @@ "packages/compass-instance": { "name": "@mongodb-js/compass-instance", "version": "4.19.1", + "extraneous": true, "license": "SSPL", "dependencies": { "@mongodb-js/compass-app-stores": "^7.6.1", @@ -47016,7 +47016,7 @@ "@testing-library/user-event": "^13.5.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "lodash": "^4.17.21", @@ -47251,7 +47251,7 @@ "@mongodb-js/webpack-config-compass": "^1.2.5", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-ipc": "^3.2.4", @@ -47467,7 +47467,7 @@ "@mongosh/logging": "^2.1.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -47764,7 +47764,7 @@ }, "optionalDependencies": { "@electron/remote": "^2.1.0", - "electron": "^25.9.6" + "electron": "^25.9.7" } }, "packages/compass-utils/node_modules/sinon": { @@ -47860,6 +47860,166 @@ "node": ">=0.3.1" } }, + "packages/compass-workspaces": { + "name": "@mongodb-js/compass-workspaces", + "version": "0.1.0", + "license": "SSPL", + "dependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", + "@mongodb-js/compass-components": "^1.19.0", + "@mongodb-js/compass-logging": "^1.2.6", + "bson": "^6.2.0", + "hadron-app-registry": "^9.0.14" + }, + "devDependencies": { + "@mongodb-js/compass-collection": "^4.19.1", + "@mongodb-js/compass-databases-collections": "^1.19.1", + "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", + "@mongodb-js/compass-serverstats": "^16.19.1", + "@mongodb-js/eslint-config-compass": "^1.0.11", + "@mongodb-js/mocha-config-compass": "^1.3.2", + "@mongodb-js/prettier-config-compass": "^1.0.1", + "@mongodb-js/tsconfig-compass": "^1.0.3", + "@mongodb-js/webpack-config-compass": "^1.2.5", + "@testing-library/react": "^12.1.4", + "@testing-library/user-event": "^13.5.0", + "@types/chai": "^4.2.21", + "@types/chai-dom": "^0.0.10", + "@types/mocha": "^9.0.0", + "@types/react": "^17.0.5", + "@types/react-dom": "^17.0.10", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "lodash": "^4.17.21", + "mocha": "^10.2.0", + "mongodb-collection-model": "^5.15.1", + "mongodb-database-model": "^2.15.1", + "mongodb-ns": "^2.4.0", + "nyc": "^15.1.0", + "prettier": "^2.7.1", + "react-dom": "^17.0.2", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "sinon": "^17.0.1", + "xvfb-maybe": "^0.2.1" + }, + "peerDependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", + "@mongodb-js/compass-components": "^1.19.0", + "@mongodb-js/compass-logging": "^1.2.6", + "bson": "^6.2.0", + "hadron-app-registry": "^9.0.14", + "react": "^17.0.2" + } + }, + "packages/compass-workspaces/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "packages/compass-workspaces/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "packages/compass-workspaces/node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "packages/compass-workspaces/node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "packages/compass-workspaces/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "packages/compass-workspaces/node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "packages/compass-workspaces/node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "packages/compass-workspaces/node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "packages/compass-workspaces/node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "packages/compass-workspaces/node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "packages/compass/node_modules/ensure-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ensure-error/-/ensure-error-3.0.1.tgz", @@ -47955,7 +48115,7 @@ "@mongodb-js/compass-user-data": "^0.1.9", "@mongodb-js/compass-utils": "^0.5.5", "bson": "^6.2.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-ipc": "^3.2.4", "keytar": "^7.9.0", "lodash": "^4.17.21", @@ -48486,7 +48646,7 @@ "hasInstallScript": true, "license": "SSPL", "dependencies": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@mongodb-js/devtools-github-repo": "^1.4.1", "@mongodb-js/dl-center": "^1.0.1", "@mongodb-js/electron-wix-msi": "^3.0.0", @@ -48501,7 +48661,7 @@ "debug": "^4.2.0", "del": "^2.0.2", "download": "^8.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "electron-packager": "^15.5.1", "electron-packager-plugin-non-proprietary-codecs-ffmpeg": "^1.0.2", "flatnest": "^1.0.0", @@ -49348,7 +49508,7 @@ "license": "SSPL", "dependencies": { "debug": "^4.3.4", - "electron": "^25.9.6", + "electron": "^25.9.7", "is-electron-renderer": "^2.0.1" }, "devDependencies": { @@ -52711,7 +52871,7 @@ "@mongodb-js/monorepo-tools": "^1.1.1", "@mongodb-js/webpack-config-compass": "^1.2.5", "commander": "^11.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "glob": "^10.2.5", "jsdom": "^21.1.0", "keytar": "^7.9.0", @@ -55572,9 +55732,9 @@ } }, "@electron/rebuild": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.3.1.tgz", - "integrity": "sha512-bQDWw9rkEGYW+gzPNFCD+ugJ8LIFSu0pORJl5fmrT+H8qETOIPAe99Klzg0wGaZRu9JN+5qLzKG+PehRGOlzmQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.4.0.tgz", + "integrity": "sha512-fi+O1zgxSmZR1X8oSOHRgCWALSS56dGHJ2AXLx9Ua3wg/NmBaMI/jpu7moU6T8lk/XRLnsC9Ds/Jo4I+UCkHAA==", "requires": { "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", @@ -58548,7 +58708,7 @@ "chai": "^4.3.6", "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-ipc": "^3.2.4", "keytar": "^7.9.0", @@ -58704,6 +58864,7 @@ "@mongodb-js/compass-app-stores": { "version": "file:packages/compass-app-stores", "requires": { + "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/mocha-config-compass": "^1.3.2", @@ -58752,6 +58913,7 @@ "@mongodb-js/compass-collection": { "version": "file:packages/compass-collection", "requires": { + "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/eslint-config-compass": "^1.0.11", @@ -58768,7 +58930,6 @@ "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", - "bson": "^6.2.0", "chai": "^4.3.6", "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", @@ -59111,7 +59272,7 @@ "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", "ejson-shell-parser": "^2.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app": "^5.15.1", @@ -59134,31 +59295,6 @@ "sinon": "^8.1.1" } }, - "@mongodb-js/compass-database": { - "version": "file:packages/compass-database", - "requires": { - "@mongodb-js/compass-components": "^1.19.0", - "@mongodb-js/compass-logging": "^1.2.6", - "@mongodb-js/eslint-config-compass": "^1.0.11", - "@mongodb-js/mocha-config-compass": "^1.3.2", - "@mongodb-js/prettier-config-compass": "^1.0.1", - "@mongodb-js/tsconfig-compass": "^1.0.3", - "@mongodb-js/webpack-config-compass": "^1.2.5", - "@testing-library/react": "^12.1.4", - "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", - "@types/react": "^17.0.5", - "@types/react-dom": "^17.0.10", - "chai": "^4.1.2", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "hadron-app-registry": "^9.0.14", - "mocha": "^10.2.0", - "nyc": "^15.1.0", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, "@mongodb-js/compass-databases-collections": { "version": "file:packages/databases-collections", "requires": { @@ -59372,7 +59508,7 @@ "d3-flextree": "2.1.2", "d3-hierarchy": "^3.1.2", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "lodash": "^4.17.21", @@ -59542,7 +59678,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "hadron-ipc": "^3.2.4", @@ -59684,11 +59820,9 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-find-in-page": "^4.19.1", "@mongodb-js/compass-import-export": "^7.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", "@mongodb-js/compass-schema-validation": "^6.20.0", @@ -59697,6 +59831,7 @@ "@mongodb-js/compass-shell": "^3.19.1", "@mongodb-js/compass-sidebar": "^5.19.1", "@mongodb-js/compass-welcome": "^0.18.1", + "@mongodb-js/compass-workspaces": "^0.1.0", "@mongodb-js/connection-storage": "^0.6.6", "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/mocha-config-compass": "^1.3.2", @@ -59758,7 +59893,7 @@ "compass-preferences-model": "^2.15.6", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "hadron-document": "^8.4.3", @@ -59882,7 +60017,7 @@ "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", "ejson-shell-parser": "^2.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", @@ -59958,36 +60093,6 @@ } } }, - "@mongodb-js/compass-instance": { - "version": "file:packages/compass-instance", - "requires": { - "@mongodb-js/compass-app-stores": "^7.6.1", - "@mongodb-js/compass-components": "^1.19.0", - "@mongodb-js/compass-logging": "^1.2.6", - "@mongodb-js/eslint-config-compass": "^1.0.11", - "@mongodb-js/mocha-config-compass": "^1.3.2", - "@mongodb-js/prettier-config-compass": "^1.0.1", - "@mongodb-js/tsconfig-compass": "^1.0.3", - "@mongodb-js/webpack-config-compass": "^1.2.5", - "@testing-library/react": "^12.1.4", - "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", - "@types/react": "^17.0.5", - "@types/react-dom": "^17.0.10", - "chai": "^4.3.4", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "hadron-app-registry": "^9.0.14", - "mocha": "^10.2.0", - "mongodb-instance-model": "^12.15.1", - "nyc": "^15.1.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-redux": "^8.0.5", - "redux": "^4.2.1", - "redux-thunk": "^2.4.1" - } - }, "@mongodb-js/compass-logging": { "version": "file:packages/compass-logging", "requires": { @@ -60114,7 +60219,7 @@ "chai": "^4.2.0", "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "lodash": "^4.17.21", @@ -60318,7 +60423,7 @@ "chai": "^4.2.0", "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", @@ -60359,7 +60464,7 @@ "@mongodb-js/webpack-config-compass": "^1.2.5", "commander": "^11.0.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "glob": "^10.2.5", "jsdom": "^21.1.0", @@ -60691,7 +60796,7 @@ "chai": "^4.2.0", "compass-preferences-model": "^2.15.6", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", @@ -60914,7 +61019,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", "mocha": "^10.2.0", @@ -61002,6 +61107,155 @@ } } }, + "@mongodb-js/compass-workspaces": { + "version": "file:packages/compass-workspaces", + "requires": { + "@mongodb-js/compass-app-stores": "^7.6.1", + "@mongodb-js/compass-collection": "^4.19.1", + "@mongodb-js/compass-components": "^1.19.0", + "@mongodb-js/compass-databases-collections": "^1.19.1", + "@mongodb-js/compass-logging": "^1.2.6", + "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", + "@mongodb-js/compass-serverstats": "^16.19.1", + "@mongodb-js/eslint-config-compass": "^1.0.11", + "@mongodb-js/mocha-config-compass": "^1.3.2", + "@mongodb-js/prettier-config-compass": "^1.0.1", + "@mongodb-js/tsconfig-compass": "^1.0.3", + "@mongodb-js/webpack-config-compass": "^1.2.5", + "@testing-library/react": "^12.1.4", + "@testing-library/user-event": "^13.5.0", + "@types/chai": "^4.2.21", + "@types/chai-dom": "^0.0.10", + "@types/mocha": "^9.0.0", + "@types/react": "^17.0.5", + "@types/react-dom": "^17.0.10", + "@types/sinon-chai": "^3.2.5", + "bson": "^6.2.0", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "hadron-app-registry": "^9.0.14", + "lodash": "^4.17.21", + "mocha": "^10.2.0", + "mongodb-collection-model": "^5.15.1", + "mongodb-database-model": "^2.15.1", + "mongodb-ns": "^2.4.0", + "nyc": "^15.1.0", + "prettier": "^2.7.1", + "react-dom": "^17.0.2", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "sinon": "^17.0.1", + "xvfb-maybe": "^0.2.1" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + } + } + }, + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + } + } + } + }, "@mongodb-js/connection-form": { "version": "file:packages/connection-form", "requires": { @@ -61080,7 +61334,7 @@ "bson": "^6.2.0", "chai": "^4.3.6", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-ipc": "^3.2.4", "keytar": "^7.9.0", @@ -63775,13 +64029,13 @@ "@types/webpack-bundle-analyzer": "^4.4.1", "babel-loader": "^8.2.5", "babel-plugin-istanbul": "^5.2.0", - "browserslist": "^4.22.1", + "browserslist": "^4.22.2", "chalk": "^4.1.2", "cli-progress": "^3.9.1", "core-js": "^3.17.3", "css-loader": "^4.3.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "html-webpack-plugin": "^5.3.2", "less-loader": "^10.0.1", @@ -71441,13 +71695,13 @@ "dev": true }, "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" } }, @@ -72163,9 +72417,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001546", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz", - "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==" + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==" }, "caseless": { "version": "0.12.0", @@ -72656,7 +72910,7 @@ "compass-e2e-tests": { "version": "file:packages/compass-e2e-tests", "requires": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@mongodb-js/compass-test-server": "^0.1.6", "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/oidc-mock-provider": "^0.4.1", @@ -72675,7 +72929,7 @@ "cross-spawn": "^7.0.3", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "fast-glob": "^3.2.7", "glob": "^10.2.5", @@ -75314,9 +75568,9 @@ } }, "electron": { - "version": "25.9.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.6.tgz", - "integrity": "sha512-vddWieRqlAFOTNZDwHwD8/3dRua8dpob6zZOUurO8y5JcFjA5/Kl2dbU6nwq/LI5BAS/6lg575xNZTJfOVwCmA==", + "version": "25.9.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.7.tgz", + "integrity": "sha512-8aejD8NricfzeoUflPPpDU7M6yEppQyASoZBT7qgiKnEy6+M3SgetZNEmkoHMGK/NNxP8GcO5lRuvCyegI1mig==", "requires": { "@electron/get": "^2.0.0", "@types/node": "^18.11.18", @@ -75817,9 +76071,9 @@ } }, "electron-to-chromium": { - "version": "1.4.542", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.542.tgz", - "integrity": "sha512-6+cpa00G09N3sfh2joln4VUXHquWrOFx3FLZqiVQvl45+zS9DskDBTPvob+BhvFRmTBkyDSk0vvLMMRo/qc6mQ==" + "version": "1.4.601", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.601.tgz", + "integrity": "sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==" }, "electron-window": { "version": "0.8.1", @@ -79152,7 +79406,7 @@ "hadron-build": { "version": "file:packages/hadron-build", "requires": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@mongodb-js/devtools-github-repo": "^1.4.1", "@mongodb-js/dl-center": "^1.0.1", "@mongodb-js/electron-wix-msi": "^3.0.0", @@ -79169,7 +79423,7 @@ "del": "^2.0.2", "depcheck": "^1.4.1", "download": "^8.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "electron-installer-debian": "^3.2.0", "electron-installer-dmg": "^4.0.0", "electron-installer-redhat": "^2.0.0", @@ -79890,7 +80144,7 @@ "chai": "^4.3.6", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "is-electron-renderer": "^2.0.1", "mocha": "^10.2.0", @@ -85581,14 +85835,13 @@ "mongodb-compass": { "version": "file:packages/compass", "requires": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@electron/remote": "^2.1.0", "@mongodb-js/atlas-service": "^0.10.1", "@mongodb-js/compass-aggregations": "^9.21.0", "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-collection": "^4.19.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-explain-plan": "^6.20.0", "@mongodb-js/compass-export-to-language": "^8.21.0", @@ -85597,7 +85850,6 @@ "@mongodb-js/compass-home": "^6.20.1", "@mongodb-js/compass-import-export": "^7.19.1", "@mongodb-js/compass-indexes": "^5.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-query-bar": "^8.20.0", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", @@ -85630,7 +85882,7 @@ "compass-preferences-model": "^2.15.6", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "electron-devtools-installer": "^3.2.0", "electron-dl": "^3.5.0", "electron-mocha": "^10.1.0", @@ -86703,9 +86955,9 @@ } }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "nopt": { "version": "6.0.0", @@ -89965,9 +90217,9 @@ } }, "react-redux": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", - "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "requires": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -90383,9 +90635,9 @@ } }, "redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==" }, "reflux": { "version": "0.4.1", diff --git a/packages/atlas-service/package.json b/packages/atlas-service/package.json index 6bbe4bbf654..8cd464064fe 100644 --- a/packages/atlas-service/package.json +++ b/packages/atlas-service/package.json @@ -79,7 +79,7 @@ "@mongodb-js/devtools-connect": "^2.4.2", "@mongodb-js/oidc-plugin": "^0.3.0", "compass-preferences-model": "^2.15.6", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-ipc": "^3.2.4", "keytar": "^7.9.0", "node-fetch": "^2.6.7", diff --git a/packages/collection-model/index.d.ts b/packages/collection-model/index.d.ts index 79075be5a5a..fd25eb3a72a 100644 --- a/packages/collection-model/index.d.ts +++ b/packages/collection-model/index.d.ts @@ -88,6 +88,8 @@ interface Collection { dataService: DataService; }): Promise; on(evt: string, fn: (...args: any) => void); + off(evt: string, fn: (...args: any) => void); + removeListener(evt: string, fn: (...args: any) => void); toJSON(opts?: { derived: boolean }): this; } diff --git a/packages/compass-app-stores/package.json b/packages/compass-app-stores/package.json index fb22b4066bb..fdeeecbd382 100644 --- a/packages/compass-app-stores/package.json +++ b/packages/compass-app-stores/package.json @@ -76,12 +76,14 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { + "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", "hadron-app-registry": "^9.0.14", "mongodb-data-service": "^22.15.1", "mongodb-instance-model": "^12.15.1" }, "peerDependencies": { + "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", "hadron-app-registry": "^9.0.14", "mongodb-data-service": "^22.15.1", diff --git a/packages/compass-app-stores/src/stores/instance-store.ts b/packages/compass-app-stores/src/stores/instance-store.ts index ffb2b4d06ce..4024f2d5159 100644 --- a/packages/compass-app-stores/src/stores/instance-store.ts +++ b/packages/compass-app-stores/src/stores/instance-store.ts @@ -4,6 +4,7 @@ import toNS from 'mongodb-ns'; import type { DataService } from 'mongodb-data-service'; import type { AppRegistry } from 'hadron-app-registry'; import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; +import { openToast } from '@mongodb-js/compass-components'; function serversArray( serversMap: NonNullable< @@ -57,6 +58,8 @@ export function createInstanceStore({ 'dataService' > = {} ) { + const isFirstRun = instance.status === 'initial'; + try { await instance.refresh({ dataService, ...refreshOptions }); @@ -71,6 +74,26 @@ export function createInstanceStore({ dataService, errorMessage: err.message, }); + + // The `instance.refresh` method is catching all expected errors: we treat + // a lot of metadata as optional so failing to fetch it shouldn't throw. + // In most cases if this failed on subsequent runs, user is probably + // already in a state that will show them a more specified error (like + // seeing some server error trying to refresh collection list in cases + // that something happened with the server after connection). However if + // we are fetching instance info for the first time (as indicated by the + // initial instance status) and we ended up here, there might be no other + // place for the user to see the error. This is a very rare case, but we + // don't want to leave the user without any indication that something went + // wrong and so we show an toast with the error message + if (isFirstRun) { + const { name, message } = err as Error; + openToast('instance-refresh-failed', { + title: 'Failed to retrieve server info', + description: `${name}: ${message}`, + variant: 'important', + }); + } } } @@ -268,13 +291,6 @@ export function createInstanceStore({ const onCollectionRenamed = voidify( async ({ from, to }: { from: string; to: string }) => { - // we must fetch the old collection's metadata before refreshing because refreshing the - // collection metadata will remove the old collection from the model. - const metadata = await fetchCollectionMetadata(from); - appRegistry.emit('refresh-collection-tabs', { - metadata, - newNamespace: to, - }); const { database } = toNS(from); await refreshNamespace({ ns: to, @@ -315,31 +331,6 @@ export function createInstanceStore({ }); onAppRegistryEvent('collection-created', onCollectionCreated); - const onActiveCollectionDropped = (ns: string) => { - // This callback will fire after drop collection happened, we force it into - // a microtask to allow drop collections event handler to force start - // databases and collections list update before we run our check here - queueMicrotask( - voidify(async () => { - const { database } = toNS(ns); - await instance.fetchDatabases({ dataService }); - const db = instance.databases.get(database); - await db?.fetchCollections({ dataService }); - if (db?.collectionsLength) { - appRegistry.emit('select-database', database); - } else { - appRegistry.emit('open-instance-workspace', 'Databases'); - } - }) - ); - }; - onAppRegistryEvent('active-collection-dropped', onActiveCollectionDropped); - - const onActiveDatabaseDropped = () => { - appRegistry.emit('open-instance-workspace', 'Databases'); - }; - onAppRegistryEvent('active-database-dropped', onActiveDatabaseDropped); - /** * Opens collection in the current active tab. No-op if currently open tab has * the same namespace. Additional `query` and `agrregation` props can be diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 724f7089d67..dc320af430e 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -56,19 +56,21 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "peerDependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", - "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", + "mongodb-data-service": "^22.15.1", "react": "^17.0.2" }, "dependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", - "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", - "hadron-app-registry": "^9.0.14" + "hadron-app-registry": "^9.0.14", + "mongodb-data-service": "^22.15.1" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.11", @@ -90,7 +92,6 @@ "eslint": "^7.25.0", "mocha": "^10.2.0", "mongodb-collection-model": "^5.15.1", - "mongodb-data-service": "^22.15.1", "mongodb-instance-model": "^12.15.1", "mongodb-ns": "^2.4.0", "numeral": "^2.0.6", diff --git a/packages/compass-collection/src/components/collection-tab.tsx b/packages/compass-collection/src/components/collection-tab.tsx index 9b32b4e06b3..d76afb2b84e 100644 --- a/packages/compass-collection/src/components/collection-tab.tsx +++ b/packages/compass-collection/src/components/collection-tab.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { connect, Provider } from 'react-redux'; +import { connect } from 'react-redux'; import type { CollectionTabPluginMetadata } from '../modules/collection-tab'; import { returnToView, @@ -14,8 +14,8 @@ import { import { css, ErrorBoundary, TabNavBar } from '@mongodb-js/compass-components'; import CollectionHeader from './collection-header'; import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; -import type { configureStore } from '../stores/collection-tab'; import { useCollectionTabPlugins } from './collection-tab-provider'; +import type { CollectionTabOptions } from '../stores/collection-tab'; const { log, mongoLogId, track } = createLoggerAndTelemetry( 'COMPASS-COLLECTION-TAB-UI' @@ -59,13 +59,15 @@ const collectionModalContainerStyles = css({ zIndex: 100, }); -const CollectionTab: React.FunctionComponent<{ +type CollectionTabProps = { currentTab: string; collectionTabPluginMetadata: CollectionTabPluginMetadata; renderScopedModals(): React.ReactElement[]; renderTabs(): { name: string; component: React.ReactElement }[]; onTabClick(name: string): void; -}> = ({ +}; + +const CollectionTab: React.FunctionComponent = ({ currentTab, collectionTabPluginMetadata, renderScopedModals, @@ -162,16 +164,6 @@ const ConnectedCollectionTab = connect( renderTabs: renderTabs, onTabClick: selectTab, } -)(CollectionTab); - -const CollectionTabPlugin: React.FunctionComponent<{ - store: ReturnType; -}> = ({ store }) => { - return ( - - - - ); -}; +)(CollectionTab) as React.FunctionComponent; -export default CollectionTabPlugin; +export default ConnectedCollectionTab; diff --git a/packages/compass-collection/src/components/workspace/index.ts b/packages/compass-collection/src/components/workspace/index.ts deleted file mode 100644 index a0e265a1eb5..00000000000 --- a/packages/compass-collection/src/components/workspace/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import MappedWorkspace from './workspace'; -export default MappedWorkspace; diff --git a/packages/compass-collection/src/components/workspace/workspace.spec.tsx b/packages/compass-collection/src/components/workspace/workspace.spec.tsx deleted file mode 100644 index 05689b46186..00000000000 --- a/packages/compass-collection/src/components/workspace/workspace.spec.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import { cleanup, render, screen } from '@testing-library/react'; - -import { Workspace } from './workspace'; - -function createTab(id: string) { - return { - id, - namespace: id, - type: 'collection', - selectedSubTabName: 'Documents', - localAppRegistry: {} as any, - component:
Tab {id} content
, - }; -} - -function renderWorkspace( - props: Partial> = {} -) { - return render( - { - /** noop */ - }} - onSelectNextTab={() => { - /** noop */ - }} - onSelectPreviousTab={() => { - /** noop */ - }} - onMoveTab={() => { - /** noop */ - }} - onCloseTab={() => { - /** noop */ - }} - onCreateNewTab={() => { - /** noop */ - }} - {...props} - > - ); -} - -describe('Workspace', function () { - afterEach(cleanup); - - it('renders the tabs', function () { - renderWorkspace(); - expect(screen.getByTitle('a - Documents')).to.exist; - expect(screen.getByTitle('b - Documents')).to.exist; - expect(screen.getByTitle('c - Documents')).to.exist; - expect(screen.getByText('Tab a content')).to.exist; - }); -}); diff --git a/packages/compass-collection/src/components/workspace/workspace.tsx b/packages/compass-collection/src/components/workspace/workspace.tsx deleted file mode 100644 index 81455c07b35..00000000000 --- a/packages/compass-collection/src/components/workspace/workspace.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, { useMemo } from 'react'; -import { connect } from 'react-redux'; -import { WorkspaceTabs, css, useHotkeys } from '@mongodb-js/compass-components'; -import { - selectNextTab, - type CollectionTab, - type CollectionTabsState, - selectPreviousTab, - moveTabByIndex, - closeTabAtIndex, - openNewTabForCurrentCollection, - selectTabByIndex, -} from '../../modules/tabs'; - -const workspaceStyles = css({ - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column', -}); - -const workspaceViewsStyles = css({ - height: '100%', - width: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - overflow: 'auto', -}); - -const workspaceViewTabStyles = css({ - height: '100%', - width: '100%', -}); - -function getIconGlyphForCollectionType(type: string) { - switch (type) { - case 'timeseries': - return 'TimeSeries'; - case 'view': - return 'Visibility'; - default: - return 'Folder'; - } -} - -/** - * The collection workspace contains tabs of multiple collections. - */ -const Workspace = ({ - tabs, - activeTabId, - onSelectTab, - onSelectNextTab, - onSelectPreviousTab, - onMoveTab, - onCloseTab, - onCreateNewTab, -}: { - tabs: CollectionTab[]; - activeTabId: string | null; - onSelectTab(index: number): void; - onSelectNextTab(): void; - onSelectPreviousTab(): void; - onMoveTab(fromIndex: number, toIndex: number): void; - onCloseTab(index: number): void; - onCreateNewTab(): void; -}) => { - const tabsForHeader = useMemo(() => { - return tabs.map((tab) => { - return { - title: tab.selectedSubTabName, - subtitle: tab.namespace, - tabContentId: tab.id, - iconGlyph: getIconGlyphForCollectionType(tab.type), - } as const; - }); - }, [tabs]); - - const selectedTabIndex = useMemo(() => { - return tabs.findIndex((tab) => tab.id === activeTabId); - }, [tabs, activeTabId]); - - const activeTab = tabs.find((tab) => tab.id === activeTabId); - - useHotkeys('ctrl + tab', onSelectNextTab); - useHotkeys('ctrl + shift + tab', onSelectPreviousTab); - useHotkeys('mod + shift + ]', onSelectNextTab); - useHotkeys('mod + shift + [', onSelectPreviousTab); - useHotkeys( - 'mod + w', - (e) => { - onCloseTab(selectedTabIndex); - // This prevents the browser from closing the window - // as this shortcut is used to exit the app. - e.preventDefault(); - }, - [selectedTabIndex] - ); - useHotkeys('mod + t', onCreateNewTab); - - return ( -
- - {activeTab && ( -
-
- {activeTab?.component} -
-
- )} -
- ); -}; - -const MappedWorkspace = connect( - (state: CollectionTabsState) => { - return { - tabs: state.tabs, - activeTabId: state.activeTabId, - }; - }, - { - onSelectTab: selectTabByIndex, - onSelectNextTab: selectNextTab, - onSelectPreviousTab: selectPreviousTab, - onMoveTab: moveTabByIndex, - onCloseTab: closeTabAtIndex, - onCreateNewTab: openNewTabForCurrentCollection, - } -)(Workspace); - -export default MappedWorkspace; -export { Workspace }; diff --git a/packages/compass-collection/src/index.ts b/packages/compass-collection/src/index.ts index fbd62d7a1cf..9f3b51bd345 100644 --- a/packages/compass-collection/src/index.ts +++ b/packages/compass-collection/src/index.ts @@ -1,36 +1,47 @@ -import type AppRegistry from 'hadron-app-registry'; -import CollectionTabsPlugin from './plugin'; -import CollectionTabsStore from './stores/tabs'; import CollectionTab from './components/collection-tab'; -import { configureStore } from './stores/collection-tab'; +import { activatePlugin as activateCollectionTabPlugin } from './stores/collection-tab'; +import { registerHadronPlugin } from 'hadron-app-registry'; +import type { DataServiceLocator } from 'mongodb-data-service/provider'; +import { dataServiceLocator } from 'mongodb-data-service/provider'; +import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider'; +import type { DataService } from 'mongodb-data-service'; -const COLLECTION_TAB_ROLE = { - name: 'CollectionTab', - component: CollectionTab, - configureStore, +export const CollectionTabPlugin = registerHadronPlugin( + { + name: 'CollectionTab', + component: CollectionTab, + activate: activateCollectionTabPlugin, + }, + { + dataService: dataServiceLocator as DataServiceLocator, + instance: mongoDBInstanceLocator, + } +); + +export const WorkspaceTab = { + name: 'Collection' as const, + component: CollectionTabPlugin, }; +export type CollectionWorkspace = { + type: typeof WorkspaceTab['name']; +} & React.ComponentProps; + /** * Activate all the components in the Collection package. - * @param {Object} appRegistry - The Hadron appRegisrty to activate this plugin with. **/ -function activate(appRegistry: AppRegistry): void { - appRegistry.registerComponent('Collection.Workspace', CollectionTabsPlugin); - appRegistry.registerStore('Collection.Store', CollectionTabsStore); - appRegistry.registerRole('CollectionTab.Content', COLLECTION_TAB_ROLE); +function activate(): void { + // noop } /** * Deactivate all the components in the Collection package. - * @param {Object} appRegistry - The Hadron appRegisrty to deactivate this plugin with. **/ -function deactivate(appRegistry: AppRegistry): void { - appRegistry.deregisterComponent('Collection.Workspace'); - appRegistry.deregisterStore('Collection.Store'); - appRegistry.deregisterRole('CollectionTab.Content', COLLECTION_TAB_ROLE); +function deactivate(): void { + // noop } -export default CollectionTabsPlugin; +export default CollectionTabPlugin; export { activate, deactivate }; export { default as metadata } from '../package.json'; export type { CollectionTabPluginMetadata } from './modules/collection-tab'; diff --git a/packages/compass-collection/src/modules/tabs.ts b/packages/compass-collection/src/modules/tabs.ts deleted file mode 100644 index 4094483e760..00000000000 --- a/packages/compass-collection/src/modules/tabs.ts +++ /dev/null @@ -1,432 +0,0 @@ -import React from 'react'; -import type { AnyAction, Reducer } from 'redux'; -import type { ThunkAction } from 'redux-thunk'; -import AppRegistry, { AppRegistryProvider } from 'hadron-app-registry'; -import { ObjectId } from 'bson'; -import type { CollectionMetadata } from 'mongodb-collection-model'; -import type { DataService } from 'mongodb-data-service'; -import toNs from 'mongodb-ns'; - -export type CollectionTab = { - id: string; - namespace: string; - type: string; - selectedSubTabName: string; - // TODO(COMPASS-7020): this doesn't belong in the state, but this is how - // collection tabs currently work, this will go away when we switch to using - // new compass-workspace plugin in combination with registerHadronPlugin - localAppRegistry: AppRegistry; - component: React.ReactElement; -}; - -export type CollectionTabsState = { - tabs: CollectionTab[]; - activeTabId: string | null; -}; - -enum CollectionTabsActions { - OpenCollection = 'compass-collection/OpenCollection', - OpenCollectionInNewTab = 'compass-collection/OpenCollectionInNewTab', - SelectTab = 'compass-collection/SelectTab', - MoveTab = 'compass-collection/MoveTab', - SelectPreviousTab = 'compass-collection/SelectPreviousTab', - SelectNextTab = 'compass-collection/SelectNextTab', - CloseTab = 'compass-collection/CloseTab', - SubtabChanged = 'compass-colection/SubtabChanged', - CollectionDropped = 'compass-collection/CollectionDropped', - DatabaseDropped = 'compass-collection/DatabaseDropped', - DataServiceConnected = 'compass-collection/DataServiceConnected', - DataServiceDisconnected = 'compass-collection/DataServiceDisconnected', - CollectionRenamed = 'compass-collection/CollectionRenamed', -} - -type CollectionTabsThunkAction< - ReturnType, - Action extends AnyAction = AnyAction -> = ThunkAction< - ReturnType, - CollectionTabsState, - { - globalAppRegistry: AppRegistry; - dataService: DataService | null; - }, - Action ->; - -const reducer: Reducer = ( - state = { tabs: [], activeTabId: null }, - action -) => { - if (action.type === CollectionTabsActions.OpenCollection) { - const activeTabIndex = getActiveTabIndex(state); - if (activeTabIndex !== -1) { - const newTabs = [...state.tabs]; - newTabs.splice(activeTabIndex, 1, action.tab); - return { - activeTabId: action.tab.id, - tabs: newTabs, - }; - } - return { - activeTabId: action.tab.id, - tabs: [...state.tabs, action.tab], - }; - } - if (action.type === CollectionTabsActions.OpenCollectionInNewTab) { - return { - activeTabId: action.tab.id, - tabs: [...state.tabs, action.tab], - }; - } - if (action.type === CollectionTabsActions.SelectTab) { - const newActiveTab = state.tabs[action.index]; - return { - ...state, - activeTabId: newActiveTab.id ?? state.activeTabId, - }; - } - if (action.type === CollectionTabsActions.SelectNextTab) { - const newActiveTabIndex = - (getActiveTabIndex(state) + 1) % state.tabs.length; - const newActiveTab = state.tabs[newActiveTabIndex]; - return { - ...state, - activeTabId: newActiveTab.id ?? state.activeTabId, - }; - } - if (action.type === CollectionTabsActions.SelectPreviousTab) { - const currentActiveTabIndex = getActiveTabIndex(state); - const newActiveTabIndex = - getActiveTabIndex(state) === 0 - ? state.tabs.length - 1 - : currentActiveTabIndex - 1; - const newActiveTab = state.tabs[newActiveTabIndex]; - return { - ...state, - activeTabId: newActiveTab.id ?? state.activeTabId, - }; - } - if (action.type === CollectionTabsActions.MoveTab) { - const newTabs = [...state.tabs]; - newTabs.splice(action.toIndex, 0, newTabs.splice(action.fromIndex, 1)[0]); - return { - ...state, - tabs: newTabs, - }; - } - if (action.type === CollectionTabsActions.CloseTab) { - const tabToClose = state.tabs[action.index]; - const tabIndex = state.tabs.findIndex((tab) => tab.id === tabToClose.id); - const newTabs = [...state.tabs]; - newTabs.splice(action.index, 1); - const newActiveTabId = - tabToClose.id === state.activeTabId - ? // We follow standard browser behavior with tabs on how we handle - // which tab gets activated if we close the active tab. If the active - // tab is the last tab, we activate the one before it, otherwise we - // activate the next tab. - (state.tabs[tabIndex + 1] ?? newTabs[newTabs.length - 1])?.id ?? null - : state.activeTabId; - return { - activeTabId: newActiveTabId, - tabs: newTabs, - }; - } - if (action.type === CollectionTabsActions.CollectionDropped) { - const newTabs = state.tabs.filter((tab) => { - return tab.namespace !== action.namespace; - }); - const isActiveTabRemoved = !newTabs.some((tab) => { - return tab.id === state.activeTabId; - }); - return { - activeTabId: isActiveTabRemoved - ? newTabs[0]?.id ?? null - : state.activeTabId, - tabs: newTabs, - }; - } - if (action.type === CollectionTabsActions.DatabaseDropped) { - const { database } = toNs(action.namespace); - const newTabs = state.tabs.filter((tab) => { - const { database: tabDatabase } = toNs(tab.namespace); - return tabDatabase !== database; - }); - const isActiveTabRemoved = !newTabs.some((tab) => { - return tab.id === state.activeTabId; - }); - return { - activeTabId: isActiveTabRemoved - ? newTabs[0]?.id ?? null - : state.activeTabId, - tabs: newTabs, - }; - } - if ( - action.type === CollectionTabsActions.DataServiceConnected || - action.type === CollectionTabsActions.DataServiceDisconnected - ) { - return { - activeTabId: null, - tabs: [], - }; - } - if (action.type === CollectionTabsActions.SubtabChanged) { - const tabIndex = state.tabs.findIndex((tab) => { - return tab.id === action.id; - }); - const tab = state.tabs[tabIndex]; - const newTabs = [...state.tabs]; - newTabs.splice(tabIndex, 1, { ...tab, selectedSubTabName: action.name }); - return { - ...state, - tabs: newTabs, - }; - } - if (action.type === CollectionTabsActions.CollectionRenamed) { - const { tabs } = action; - - const activeTabIndex = getActiveTabIndex(state); - const activeTabId = tabs[activeTabIndex]?.id ?? null; - return { - ...state, - tabs, - activeTabId, - }; - } - return state; -}; - -const subtabChanged = (id: string, name: string) => { - return { type: CollectionTabsActions.SubtabChanged, id, name }; -}; - -const createNewTab = ( - collectionMetadata: CollectionMetadata -): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry, dataService }) => { - const collectionTabRole = globalAppRegistry.getRole( - 'CollectionTab.Content' - )?.[0]; - if (!collectionTabRole || !collectionTabRole.configureStore) { - throw new Error( - "Can't open a colleciton tab if collection tab role is not registered" - ); - } - if (!dataService) { - throw new Error( - "Can't open a collection tab while data service is not connected" - ); - } - const localAppRegistry = new AppRegistry(); - const store = collectionTabRole.configureStore({ - dataService, - globalAppRegistry, - localAppRegistry, - ...collectionMetadata, - }); - const component = React.createElement(AppRegistryProvider, { - localAppRegistry: localAppRegistry, - deactivateOnUnmount: false, - children: React.createElement(collectionTabRole.component, { - store, - }), - }); - const tab: CollectionTab = { - id: new ObjectId().toHexString(), - selectedSubTabName: store.getState().currentTab, - namespace: collectionMetadata.namespace, - type: collectionMetadata.isTimeSeries - ? 'timeseries' - : collectionMetadata.isReadonly - ? 'view' - : 'collection', - localAppRegistry, - component, - }; - localAppRegistry.on('subtab-changed', (name: string) => { - dispatch(subtabChanged(tab.id, name)); - }); - return tab; - }; -}; - -export const openCollectionInNewTab = ( - // NB: now that we have clean separation between tabs and collection content, - // we can make collection fetch its own metadata without the need for this to - // happen in instance store - collectionMetadata: CollectionMetadata -): CollectionTabsThunkAction => { - return (dispatch) => { - const tab = dispatch(createNewTab(collectionMetadata)); - dispatch({ type: CollectionTabsActions.OpenCollectionInNewTab, tab }); - }; -}; - -export const openCollection = ( - collectionMetadata: CollectionMetadata -): CollectionTabsThunkAction => { - return (dispatch, getState) => { - // If current active tab namespace is the same, do nothing - if (getActiveTab(getState())?.namespace === collectionMetadata.namespace) { - return; - } - const tab = dispatch(createNewTab(collectionMetadata)); - dispatch({ type: CollectionTabsActions.OpenCollection, tab }); - }; -}; - -export const selectTabByIndex = ( - index: number -): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry }) => { - dispatch({ type: CollectionTabsActions.SelectTab, index }); - // NB: this will cause `openTab` action to dispatch, but it will be a no-op - // as we are "selecting" already selected namespace. This is needed so that - // other parts of the application can sync namespace correctly when user - // switches between multiple open tabs - globalAppRegistry.emit('collection-workspace-select-namespace', { - ns: getActiveTab(getState())?.namespace, - }); - }; -}; - -export const selectPreviousTab = (): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry }) => { - dispatch({ type: CollectionTabsActions.SelectPreviousTab }); - globalAppRegistry.emit('collection-workspace-select-namespace', { - ns: getActiveTab(getState())?.namespace, - }); - }; -}; - -export const selectNextTab = (): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry }) => { - dispatch({ type: CollectionTabsActions.SelectNextTab }); - globalAppRegistry.emit('collection-workspace-select-namespace', { - ns: getActiveTab(getState())?.namespace, - }); - }; -}; - -export const moveTabByIndex = ( - fromIndex: number, - toIndex: number -): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry }) => { - dispatch({ type: CollectionTabsActions.MoveTab, fromIndex, toIndex }); - globalAppRegistry.emit('collection-workspace-select-namespace', { - ns: getActiveTab(getState())?.namespace, - }); - }; -}; - -export const closeTabAtIndex = ( - index: number -): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry }) => { - const lastActiveTab = getActiveTab(getState()); - dispatch({ type: CollectionTabsActions.CloseTab, index }); - if (lastActiveTab && getState().tabs.length === 0) { - const { database } = toNs(lastActiveTab.namespace); - globalAppRegistry.emit('select-database', database); - } - }; -}; - -export const getActiveTabIndex = (state: CollectionTabsState) => { - const { activeTabId, tabs } = state; - return tabs.findIndex((tab) => tab.id === activeTabId); -}; - -export const getActiveTab = ( - state: CollectionTabsState -): CollectionTab | null => { - return state.tabs[getActiveTabIndex(state)] ?? null; -}; - -export const openNewTabForCurrentCollection = - (): CollectionTabsThunkAction => { - return (dispatch, getState, { globalAppRegistry }) => { - const activeTab = getActiveTab(getState()); - // Create new tab always uses the current active tab namespace, without - // active tab, we can't create new tab - if (!activeTab) { - throw new Error("Can't create new tab when no tabs are on the screen"); - } - // TODO(COMPASS-7020): we can remove this indirection when moving the - // logic to compass-workspace plugin and make sure that compass-collection - // tab is responsible for getting all required metadata - globalAppRegistry.emit( - 'collection-workspace-open-collection-in-new-tab', - { ns: activeTab.namespace } - ); - }; - }; - -export const collectionDropped = ( - namespace: string -): CollectionTabsThunkAction => { - return (dispath, getState, { globalAppRegistry }) => { - const lastActiveTab = getActiveTab(getState()); - dispath({ type: CollectionTabsActions.CollectionDropped, namespace }); - // We just removed last tab, emit event and let instance store figure out - // what to open based on that - if (lastActiveTab && getState().tabs.length === 0) { - globalAppRegistry.emit( - 'active-collection-dropped', - lastActiveTab.namespace - ); - } - }; -}; - -export const databaseDropped = ( - namespace: string -): CollectionTabsThunkAction => { - return (dispath, getState, { globalAppRegistry }) => { - const lastActiveTab = getActiveTab(getState()); - dispath({ type: CollectionTabsActions.DatabaseDropped, namespace }); - if (lastActiveTab && getState().tabs.length === 0) { - const { database } = toNs(lastActiveTab.namespace); - globalAppRegistry.emit('active-database-dropped', database); - } - }; -}; - -export const dataServiceConnected = () => { - return { type: CollectionTabsActions.DataServiceConnected }; -}; - -export const dataServiceDisconnected = () => { - // TODO: get localAppRegistry for existing tabs and clean up - return { type: CollectionTabsActions.DataServiceDisconnected }; -}; - -export const collectionRenamed = ({ - from, - newNamespace, -}: { - from: CollectionMetadata; - newNamespace: string; -}): CollectionTabsThunkAction => { - return (dispatch, getState) => { - const tabs = getState().tabs.map((tab) => - tab.namespace === from.namespace - ? dispatch( - createNewTab({ - ...from, - namespace: newNamespace, - }) - ) - : tab - ); - - dispatch({ - type: CollectionTabsActions.CollectionRenamed, - tabs, - }); - }; -}; - -export default reducer; diff --git a/packages/compass-collection/src/plugin.tsx b/packages/compass-collection/src/plugin.tsx deleted file mode 100644 index 97d37b4b771..00000000000 --- a/packages/compass-collection/src/plugin.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { Component } from 'react'; -import Workspace from './components/workspace'; -import { Provider } from 'react-redux'; -import store from './stores/tabs'; - -class Plugin extends Component { - static displayName = 'CollectionWorkspacePlugin'; - - /** - * Connect the Plugin to the store and render. - * - * @returns {React.Component} The rendered component. - */ - render() { - return ( - - - - ); - } -} - -export default Plugin; -export { Plugin }; diff --git a/packages/compass-collection/src/stores/collection-tab.spec.ts b/packages/compass-collection/src/stores/collection-tab.spec.ts index ab8d115e022..923dc734cf8 100644 --- a/packages/compass-collection/src/stores/collection-tab.spec.ts +++ b/packages/compass-collection/src/stores/collection-tab.spec.ts @@ -1,5 +1,5 @@ import type { CollectionTabOptions } from './collection-tab'; -import { configureStore as _configureStore } from './collection-tab'; +import { activatePlugin } from './collection-tab'; import { selectTab, selectDatabase, @@ -30,7 +30,10 @@ describe('Collection Tab Content store', function () { databases: { get() {} }, dataLake: {}, build: {}, + removeListener() {}, } as any; + let store: ReturnType['store']; + let deactivate: ReturnType['deactivate']; const scopedModalRole = { name: 'ScopedModal', @@ -51,21 +54,23 @@ describe('Collection Tab Content store', function () { }; const configureStore = (options: Partial = {}) => { - return _configureStore({ - dataService, - globalAppRegistry, - localAppRegistry, - ...defaultMetadata, - ...options, - }); + ({ store, deactivate } = activatePlugin( + { + ...defaultMetadata, + ...options, + }, + { + dataService, + globalAppRegistry, + localAppRegistry, + instance, + }, + { on() {}, cleanup() {} } as any + )); + return store; }; beforeEach(function () { - globalAppRegistry.registerStore('App.InstanceStore', { - getState() { - return { instance }; - }, - } as any); globalAppRegistry.registerRole( 'Collection.ScopedModal', scopedModalRole as any @@ -88,6 +93,7 @@ describe('Collection Tab Content store', function () { localAppRegistry.components = {}; sandbox.resetHistory(); + deactivate(); }); describe('selectTab', function () { diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index bc3323d60f1..f44c7e905f6 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -11,22 +11,28 @@ import type Collection from 'mongodb-collection-model'; import toNs from 'mongodb-ns'; import type { MongoDBInstance } from 'mongodb-instance-model'; import type { CollectionMetadata } from 'mongodb-collection-model'; +import type { ActivateHelpers } from 'hadron-app-registry'; export type CollectionTabOptions = { - dataService: DataService; - globalAppRegistry: AppRegistry; - localAppRegistry: AppRegistry; query?: unknown; aggregation?: unknown; pipelineText?: string; editViewName?: string; -} & CollectionMetadata; +} & CollectionMetadata; // TODO: make collection-tab resovle metadata on its own + +export type CollectionTabServices = { + dataService: DataService; + globalAppRegistry: AppRegistry; + localAppRegistry: AppRegistry; + instance: MongoDBInstance; +}; -export function configureStore(options: CollectionTabOptions) { +export function activatePlugin( + options: CollectionTabOptions, + services: CollectionTabServices, + { on, cleanup }: ActivateHelpers +) { const { - dataService, - globalAppRegistry, - localAppRegistry, query, aggregation, editViewName, @@ -34,17 +40,8 @@ export function configureStore(options: CollectionTabOptions) { ...collectionMetadata } = options; - const instance = ( - globalAppRegistry.getStore('App.InstanceStore') as - | { - getState(): { instance: MongoDBInstance }; - } - | undefined - )?.getState().instance; - - if (!instance) { - throw new Error('Expected to get instance from App.InstanceStore'); - } + const { dataService, globalAppRegistry, localAppRegistry, instance } = + services; const configureFieldStore = globalAppRegistry.getStore( 'Field.Store' @@ -100,23 +97,32 @@ export function configureStore(options: CollectionTabOptions) { ) ); - collectionModel?.on('change:status', (model: Collection, status: string) => { - if (status === 'ready') { - store.dispatch(collectionStatsFetched(model)); - } - }); - - localAppRegistry.on('open-create-index-modal', () => { + on(localAppRegistry, 'open-create-index-modal', () => { store.dispatch(selectTab('Indexes')); }); - localAppRegistry.on('open-create-search-index-modal', () => { + on(localAppRegistry, 'open-create-search-index-modal', () => { store.dispatch(selectTab('Indexes')); }); - localAppRegistry.on('generate-aggregation-from-query', () => { + on(localAppRegistry, 'generate-aggregation-from-query', () => { store.dispatch(selectTab('Aggregations')); }); - return store; + if (collectionModel) { + on( + collectionModel, + 'change:status', + (model: Collection, status: string) => { + if (status === 'ready') { + store.dispatch(collectionStatsFetched(model)); + } + } + ); + } + + return { + store, + deactivate: cleanup, + }; } diff --git a/packages/compass-collection/src/stores/tabs.spec.ts b/packages/compass-collection/src/stores/tabs.spec.ts deleted file mode 100644 index 96549287d67..00000000000 --- a/packages/compass-collection/src/stores/tabs.spec.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { expect } from 'chai'; -import { configureStore } from './tabs'; -import { - openCollection, - openCollectionInNewTab, - openNewTabForCurrentCollection, - selectNextTab, - selectPreviousTab, - selectTabByIndex, - moveTabByIndex, - closeTabAtIndex, - getActiveTab, - databaseDropped, - collectionDropped, -} from '../modules/tabs'; -import Sinon from 'sinon'; -import AppRegistry from 'hadron-app-registry'; -import type { DataService } from 'mongodb-data-service'; - -describe('Collection Tabs Store', function () { - const sandbox = Sinon.createSandbox(); - const globalAppRegistry = sandbox.spy(new AppRegistry()); - const dataService = sandbox.spy({ - isConnected() { - return true; - }, - } as DataService); - const CollectionTabRole = { - name: 'CollectionTab', - component: () => null, - configureStore: sandbox.stub().returns({ - getState() { - return { currentTab: 'Documents' }; - }, - }), - }; - - before(function () { - globalAppRegistry.registerRole('CollectionTab.Content', CollectionTabRole); - }); - - afterEach(function () { - sandbox.resetHistory(); - }); - - describe('openCollectionInNewTab', function () { - it('should set up new collection tab', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.buz' } as any)); - - const state = store.getState(); - - expect(state).to.have.property('tabs').have.lengthOf(3); - expect(state.tabs.map((tab) => tab.namespace)).to.deep.eq([ - 'test.foo', - 'test.bar', - 'test.buz', - ]); - expect(state.tabs[0].localAppRegistry).not.eq( - state.tabs[1].localAppRegistry - ); - expect(state.tabs[0].localAppRegistry).not.eq( - state.tabs[2].localAppRegistry - ); - expect(state.tabs[1].localAppRegistry).not.eq( - state.tabs[2].localAppRegistry - ); - expect(CollectionTabRole.configureStore).to.have.been.calledThrice; - }); - }); - - describe('openCollection', function () { - it('should open collection in the same tab', function () { - const store = configureStore({ globalAppRegistry, dataService }); - - store.dispatch(openCollection({ namespace: 'test.foo' } as any)); - expect(store.getState()).to.have.property('tabs').have.lengthOf(1); - expect(store.getState()).to.have.nested.property( - 'tabs[0].namespace', - 'test.foo' - ); - - store.dispatch(openCollection({ namespace: 'test.bar' } as any)); - expect(store.getState()).to.have.property('tabs').have.lengthOf(1); - expect(store.getState()).to.have.nested.property( - 'tabs[0].namespace', - 'test.bar' - ); - - store.dispatch(openCollection({ namespace: 'test.buz' } as any)); - expect(store.getState()).to.have.property('tabs').have.lengthOf(1); - expect(store.getState()).to.have.nested.property( - 'tabs[0].namespace', - 'test.buz' - ); - }); - - it('should do nothing when opening tab for the same namespace as the active tab', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollection({ namespace: 'test.foo' } as any)); - const stateWithOneTab = store.getState(); - - store.dispatch(openCollection({ namespace: 'test.foo' } as any)); - store.dispatch(openCollection({ namespace: 'test.foo' } as any)); - store.dispatch(openCollection({ namespace: 'test.foo' } as any)); - - expect(store.getState()).to.eq(stateWithOneTab); - }); - }); - - describe('openNewTabForCurrentCollection', function () { - it('should emit an event to open new tab with the same namespace as the active tab', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollection({ namespace: 'test.foo' } as any)); - store.dispatch(openNewTabForCurrentCollection()); - expect(globalAppRegistry.emit).to.have.been.calledWith( - 'collection-workspace-open-collection-in-new-tab', - { ns: 'test.foo' } - ); - }); - }); - - describe('selectNextTab', function () { - it('should select next tab circular', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.buz' } as any)); - - store.dispatch(selectNextTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.foo' - ); - - store.dispatch(selectNextTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.bar' - ); - - store.dispatch(selectNextTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.buz' - ); - - store.dispatch(selectNextTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.foo' - ); - }); - }); - - describe('selectPreviousTab', function () { - it('should select previous tab circular', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.buz' } as any)); - - store.dispatch(selectPreviousTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.bar' - ); - - store.dispatch(selectPreviousTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.foo' - ); - - store.dispatch(selectPreviousTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.buz' - ); - - store.dispatch(selectPreviousTab()); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.bar' - ); - }); - }); - - describe('selectTabByIndex', function () { - it('should select tab by index', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.buz' } as any)); - - store.dispatch(selectTabByIndex(0)); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.foo' - ); - - store.dispatch(selectTabByIndex(1)); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.bar' - ); - - store.dispatch(selectTabByIndex(2)); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.buz' - ); - }); - }); - - describe('moveTabByIndex', function () { - it('should move tab by index without changing active tab', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.buz' } as any)); - - store.dispatch(moveTabByIndex(2, 0)); - expect( - store.getState().tabs.map((tab) => { - return tab.namespace; - }) - ).to.deep.eq(['test.buz', 'test.foo', 'test.bar']); - expect(getActiveTab(store.getState())).to.have.property( - 'namespace', - 'test.buz' - ); - }); - }); - - describe('closeTabAtIndex', function () { - it('should remove the tab on close', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.buz' } as any)); - store.dispatch(closeTabAtIndex(0)); - expect( - store.getState().tabs.map((tab) => { - return tab.namespace; - }) - ).to.deep.eq(['test.bar', 'test.buz']); - }); - - it("should emit 'select-database' when last tab is closed", function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(closeTabAtIndex(0)); - expect(store.getState().tabs).to.have.lengthOf(0); - expect(globalAppRegistry.emit).to.have.been.calledWith( - 'select-database', - 'test' - ); - }); - }); - - describe('databaseDropped', function () { - it('should remove all tabs with dropped database', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'meow.buz' } as any)); - store.dispatch(databaseDropped('test')); - expect( - store.getState().tabs.map((tab) => { - return tab.namespace; - }) - ).to.deep.eq(['meow.buz']); - }); - - it("should emit 'active-database-dropped' when action closes all open tabs", function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.bar' } as any)); - store.dispatch(databaseDropped('test')); - expect(store.getState().tabs).to.have.lengthOf(0); - expect(globalAppRegistry.emit).to.have.been.calledWith( - 'active-database-dropped', - 'test' - ); - }); - }); - - describe('collectionDropped', function () { - it('should remove all tabs with dropped collection', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'meow.buz' } as any)); - store.dispatch(collectionDropped('test.foo')); - expect( - store.getState().tabs.map((tab) => { - return tab.namespace; - }) - ).to.deep.eq(['meow.buz']); - }); - - it("should emit 'active-collection-dropped' when action closes all open tabs", function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(openCollectionInNewTab({ namespace: 'test.foo' } as any)); - store.dispatch(collectionDropped('test.foo')); - expect(store.getState().tabs).to.have.lengthOf(0); - expect(globalAppRegistry.emit).to.have.been.calledWith( - 'active-collection-dropped', - 'test.foo' - ); - }); - }); - - describe('onActivated', function () { - it('should set up listeners on globalAppRegistry', function () { - const store = configureStore({ globalAppRegistry, dataService }); - store.onActivated(globalAppRegistry); - expect(globalAppRegistry['_emitter'].eventNames()).to.deep.eq([ - 'open-namespace-in-new-tab', - 'select-namespace', - 'collection-dropped', - 'database-dropped', - 'refresh-collection-tabs', - 'data-service-connected', - 'data-service-disconnected', - 'menu-share-schema-json', - 'open-active-namespace-export', - 'open-active-namespace-import', - ]); - }); - }); -}); diff --git a/packages/compass-collection/src/stores/tabs.ts b/packages/compass-collection/src/stores/tabs.ts deleted file mode 100644 index 3489d6820e4..00000000000 --- a/packages/compass-collection/src/stores/tabs.ts +++ /dev/null @@ -1,167 +0,0 @@ -import type AppRegistry from 'hadron-app-registry'; -import type { AnyAction, Store } from 'redux'; -import { createStore, applyMiddleware } from 'redux'; -import type { ThunkDispatch } from 'redux-thunk'; -import thunk from 'redux-thunk'; -import type { DataService } from 'mongodb-data-service'; -import type { CollectionTabsState } from '../modules/tabs'; -import tabs, { - collectionDropped, - databaseDropped, - openCollectionInNewTab, - openCollection, - getActiveTab, - dataServiceDisconnected, - dataServiceConnected, - collectionRenamed, -} from '../modules/tabs'; -import { globalAppRegistry } from 'hadron-app-registry'; -import type { CollectionMetadata } from 'mongodb-collection-model'; - -type ThunkExtraArg = { - globalAppRegistry: AppRegistry; - dataService: DataService | null; -}; - -type RootStore = Store & { - dispatch: ThunkDispatch< - any, - { - globalAppRegistry: Readonly; - dataService: DataService | null; - }, - AnyAction - >; -} & { - onActivated(globalAppRegistry: AppRegistry): void; -}; - -export function configureStore({ - globalAppRegistry: _globalAppRegistry, - dataService, -}: Partial = {}): RootStore { - const thunkExtraArg = { - globalAppRegistry: _globalAppRegistry ?? globalAppRegistry, - dataService: dataService ?? null, - }; - - const store = createStore( - tabs, - applyMiddleware(thunk.withExtraArgument(thunkExtraArg)) - ); - - Object.assign(store, { - onActivated: (globalAppRegistry: AppRegistry) => { - thunkExtraArg.globalAppRegistry = globalAppRegistry; - /** - * When emitted, will always open a collection namespace in new tab - */ - globalAppRegistry.on('open-namespace-in-new-tab', (metadata) => { - if (!metadata.namespace) { - return; - } - store.dispatch(openCollectionInNewTab(metadata)); - }); - - /** - * When emitted, will either replace content of the current tab if namespace - * doesn't match current tab namespace, or will do nothing when "selecting" - * namespace is the same as currently active - */ - globalAppRegistry.on('select-namespace', (metadata) => { - if (!metadata.namespace) { - return; - } - store.dispatch(openCollection(metadata)); - }); - - globalAppRegistry.on('collection-dropped', (namespace: string) => { - store.dispatch(collectionDropped(namespace)); - }); - - globalAppRegistry.on('database-dropped', (namespace: string) => { - store.dispatch(databaseDropped(namespace)); - }); - - globalAppRegistry.on( - 'refresh-collection-tabs', - ({ - metadata, - newNamespace, - }: { - metadata: CollectionMetadata; - newNamespace: string; - }) => { - store.dispatch( - collectionRenamed({ - from: metadata, - newNamespace, - }) - ); - } - ); - - /** - * Set the data service in the store when connected. - */ - globalAppRegistry.on( - 'data-service-connected', - (error, dataService: DataService) => { - thunkExtraArg.dataService = dataService; - store.dispatch(dataServiceConnected()); - } - ); - - /** - * When we disconnect from the instance, clear all the tabs. - */ - globalAppRegistry.on('data-service-disconnected', () => { - store.dispatch(dataServiceDisconnected()); - thunkExtraArg.dataService = null; - }); - - globalAppRegistry.on('menu-share-schema-json', () => { - const activeTab = getActiveTab(store.getState()); - if (!activeTab) { - return; - } - activeTab.localAppRegistry.emit('menu-share-schema-json'); - }); - - globalAppRegistry.on('open-active-namespace-export', function () { - const activeTab = getActiveTab(store.getState()); - - if (!activeTab) { - return; - } - - globalAppRegistry.emit('open-export', { - exportFullCollection: true, - namespace: activeTab.namespace, - origin: 'menu', - }); - }); - - globalAppRegistry.on('open-active-namespace-import', function () { - const activeTab = getActiveTab(store.getState()); - - if (!activeTab) { - return; - } - - globalAppRegistry.emit('open-import', { - namespace: activeTab.namespace, - origin: 'menu', - }); - }); - }, - }); - - return store as RootStore; -} - -const store = configureStore(); - -export type RootState = ReturnType; - -export default store; diff --git a/packages/compass-components/src/components/resizeable-sidebar.tsx b/packages/compass-components/src/components/resizeable-sidebar.tsx index 034d6b0cf77..17a222770df 100644 --- a/packages/compass-components/src/components/resizeable-sidebar.tsx +++ b/packages/compass-components/src/components/resizeable-sidebar.tsx @@ -67,6 +67,9 @@ const ResizableSidebar = ({ minWidth = 210, collapsedWidth = 48, children, + className, + style, + ...props }: { collapsable?: boolean; expanded?: boolean; @@ -75,7 +78,7 @@ const ResizableSidebar = ({ minWidth?: number; collapsedWidth?: number; children: JSX.Element; -}): JSX.Element => { +} & React.HTMLProps): JSX.Element => { const darkMode = useDarkMode(); const [width, setWidth] = useState(initialWidth); const [prevWidth, setPrevWidth] = useState(initialWidth); @@ -126,13 +129,16 @@ const ResizableSidebar = ({
{children} ; @@ -206,7 +163,7 @@ type TabProps = { onClose: () => void; iconGlyph: IconGlyph; tabContentId: string; - subtitle: string; + subtitle?: string; }; function Tab({ @@ -220,30 +177,16 @@ function Tab({ subtitle, }: TabProps) { const darkMode = useDarkMode(); - - const [focusProps, focusState] = useFocusState(); - - const isFocused = useMemo( - () => focusState === FocusState.FocusVisible, - [focusState] - ); - const isFocusedWithin = useMemo( - () => focusState === FocusState.FocusWithinVisible, - [focusState] - ); const defaultActionProps = useDefaultAction(onSelect); - - const [hoverProps, isHovered] = useHoverState(); + const { listeners, setNodeRef, transform, transition } = useSortable({ + id: tabContentId, + }); const tabProps = mergeProps( - focusProps, - hoverProps, - defaultActionProps + defaultActionProps, + listeners ?? {} ); - const { listeners, setNodeRef, transform, transition } = useSortable({ - id: tabContentId, - }); const style = { transform: cssDndKit.Transform.toString(transform), transition, @@ -257,11 +200,9 @@ function Tab({ className={cx( tabStyles, darkMode ? tabDarkThemeStyles : tabLightThemeStyles, - { - [selectedTabStyles]: isSelected, - [focusedTabStyles]: isFocused, - [draggingTabStyles]: isDragging, - } + isSelected && selectedTabStyles, + isDragging && draggingTabStyles, + subtitle && animatedSubtitleStyles )} aria-selected={isSelected} role="tab" @@ -269,56 +210,32 @@ function Tab({ tabIndex={isSelected ? 0 : -1} aria-controls={tabContentId} data-testid="workspace-tab-button" - title={`${subtitle} - ${title}`} - {...listeners} + title={subtitle ? subtitle : title} {...tabProps} > + +
- -
- {title} -
- - {subtitle} - +
{title}
+ {subtitle && ( +
+ {subtitle} +
+ )}
{ e.stopPropagation(); onClose(); @@ -328,12 +245,6 @@ function Tab({ > -
); } diff --git a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx index 5b58ad63c0c..b60bf9e335b 100644 --- a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx +++ b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx @@ -11,7 +11,7 @@ function mockTab(tabId: number): TabProps { return { title: `mock-tab-${tabId}`, subtitle: `Documents - ${tabId}`, - tabContentId: `${tabId}-content`, + id: `${tabId}-content`, iconGlyph: 'Folder', }; } @@ -20,12 +20,16 @@ describe('WorkspaceTabs', function () { let onCreateNewTabSpy: sinon.SinonSpy; let onCloseTabSpy: sinon.SinonSpy; let onSelectSpy: sinon.SinonSpy; + let onSelectNextSpy: sinon.SinonSpy; + let onSelectPrevSpy: sinon.SinonSpy; let onMoveTabSpy: sinon.SinonSpy; beforeEach(function () { onCreateNewTabSpy = sinon.spy(); onCloseTabSpy = sinon.spy(); onSelectSpy = sinon.spy(); + onSelectNextSpy = sinon.spy(); + onSelectPrevSpy = sinon.spy(); onMoveTabSpy = sinon.spy(); }); @@ -39,6 +43,8 @@ describe('WorkspaceTabs', function () { onCreateNewTab={onCreateNewTabSpy} onCloseTab={onCloseTabSpy} onSelectTab={onSelectSpy} + onSelectNextTab={onSelectNextSpy} + onSelectPrevTab={onSelectPrevSpy} onMoveTab={onMoveTabSpy} tabs={[]} selectedTabIndex={0} diff --git a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx index 22568488241..1cd3596a534 100644 --- a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx +++ b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx @@ -29,6 +29,7 @@ import { FocusState, useFocusState } from '../../hooks/use-focus-hover'; import { Icon, IconButton } from '../leafygreen'; import { mergeProps } from '../../utils/merge-props'; import { Tab } from './tab'; +import { useHotkeys } from '../../hooks/use-hotkeys'; export const scrollbarThumbLightTheme = rgba(palette.gray.base, 0.65); export const scrollbarThumbDarkTheme = rgba(palette.gray.base, 0.65); @@ -40,36 +41,33 @@ const tabsContainerStyles = css({ position: 'relative', overflow: 'overlay', whiteSpace: 'nowrap', - borderBottom: '1px solid', '::-webkit-scrollbar': { ':horizontal': { height: spacing[1], }, }, + minHeight: 36, }); const tabsContainerLightStyles = css({ - background: palette.white, - borderBottomColor: palette.gray.light2, + background: palette.gray.light3, + boxShadow: `inset 0px -1px 0 0 ${palette.gray.light2}`, '::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumbLightTheme, }, }); const tabsContainerDarkStyles = css({ - backgroundColor: palette.black, - borderBottomColor: palette.gray.dark2, + backgroundColor: palette.gray.dark3, + boxShadow: `inset 0px -1px 0 0 ${palette.gray.dark2}`, '::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumbDarkTheme, }, }); const tabsListContainerStyles = css({ - padding: 0, - paddingRight: spacing[4], display: 'flex', flexDirection: 'row', - alignItems: 'center', }); const tabsListStyles = css({ @@ -77,14 +75,12 @@ const tabsListStyles = css({ }); const newTabContainerStyles = css({ - display: 'inline-flex', - flexDirection: 'row', - alignItems: 'center', + flex: 'none', + alignSelf: 'center', }); const createNewTabButtonStyles = css({ - marginLeft: spacing[2], - marginRight: spacing[2], + margin: spacing[1], }); const sortableItemContainerStyles = css({ @@ -164,6 +160,8 @@ type WorkspaceTabsProps = { 'aria-label': string; onCreateNewTab: () => void; onSelectTab: (tabIndex: number) => void; + onSelectNextTab: () => void; + onSelectPrevTab: () => void; onCloseTab: (tabIndex: number) => void; onMoveTab: (oldTabIndex: number, newTabIndex: number) => void; tabs: TabProps[]; @@ -171,9 +169,9 @@ type WorkspaceTabsProps = { }; export type TabProps = { - subtitle: string; - tabContentId: string; + id: string; title: string; + subtitle?: string; iconGlyph: Extract; }; @@ -215,7 +213,7 @@ const SortableList = ({ selectedTabIndex, onClose, }: SortableListProps) => { - const items = tabs.map((tab) => tab.tabContentId); + const items = tabs.map((tab) => tab.id); const [activeId, setActiveId] = useState(null); const sensors = useSensors( useSensor(MouseSensor, { @@ -235,8 +233,8 @@ const SortableList = ({ const onSortEnd = useCallback( ({ oldIndex, newIndex }) => { - const from = tabs.findIndex((tab) => tab.tabContentId === oldIndex); - const to = tabs.findIndex((tab) => tab.tabContentId === newIndex); + const from = tabs.findIndex((tab) => tab.id === oldIndex); + const to = tabs.findIndex((tab) => tab.id === newIndex); onMove(from, to); }, [onMove, tabs] @@ -265,7 +263,7 @@ const SortableList = ({
{tabs.map((tab: TabProps, index: number) => ( tabContentId === activeId, - [tabContentId, activeId] - ); + const isDragging = useMemo(() => id === activeId, [id, activeId]); return ( { + e.preventDefault(); + e.stopPropagation(); + onSelectNextTab(); + }, + [onSelectNextTab] + ); + useHotkeys( + 'ctrl + shift + tab', + (e) => { + e.preventDefault(); + e.stopPropagation(); + onSelectPrevTab(); + }, + [onSelectPrevTab] + ); + useHotkeys( + 'mod + shift + ]', + (e) => { + e.preventDefault(); + e.stopPropagation(); + onSelectNextTab(); + }, + [onSelectNextTab] + ); + useHotkeys( + 'mod + shift + [', + (e) => { + e.preventDefault(); + e.stopPropagation(); + onSelectPrevTab(); + }, + [onSelectPrevTab] + ); + useHotkeys( + 'mod + w', + (e) => { + e.preventDefault(); + e.stopPropagation(); + onCloseTab(selectedTabIndex); + }, + [onCloseTab, selectedTabIndex] + ); + useHotkeys( + 'mod + t', + (e) => { + e.preventDefault(); + e.stopPropagation(); + onCreateNewTab(); + }, + [onCreateNewTab] + ); + return (
Compass Database Plugin - -## Usage - -### Scripts - -`link-plugin`: Links the Compass plugin and Compass for development along with React to ensure the -plugin and Compass are using the same React instance. - -```shell -COMPASS_HOME=/path/to/my/compass npm run link-plugin -``` - -`unlink-plugin`: Restores Compass and the plugin to their original unlinked state. - -```shell -COMPASS_HOME=/path/to/my/compass npm run unlink-plugin -``` - -## Features - -#### Electron - -Validate and test your component in an Electron window, styles included. The source automatically -compiles and the window content reloads when any file under `./src` changes. - -To start Electron and render your component, type `npm start`. - -#### Enzyme - -The test environment is configured to test components with [Enzyme][enzyme] -(including full `mount` mode through [jsdom][jsdom]) and [enzyme-chai][enzyme-chai]. -See the test folder for examples. Run `npm test` to execute the test suite. - -## Developing - -Almost all of your development will happen in the `./src` directory. Add new components -to `./src/components`, actions to `./src/actions/index.js` and if you need additional -stores, add them to `./src/stores`. - -To be able to debug the plugin inside `compass` make sure [webpack prod -config](./config/webpack.prod.config.js) has `devtool` is set to `source-map`. -If you want faster compiler time when you commit/push, switch it to `false.` - -```js -const config = { - target: 'electron-renderer', - devtool: 'source-map', -}; -``` - -#### Directory Structure - -For completeness, below is a list of directories present in this module: - -- `electron` code to start electron, open a browser window and load the source. - You don't usually need to touch this, unless you want to render something other - than the main component in Electron. -- `lib` compiled version of your components (plain javascript instead of `jsx`) and - styles (`css` instead of `less`). Never change anything here as this entire folder - gets automatically created and overwritten. -- `src` components, actions and stores source code, as well as style files. This is the - place to implement your own components. `npm run compile` will use `./src` as input - and create `./lib`. -- `test` implement your tests here, and name the files `*.test.js`. - -[enzyme]: http://airbnb.io/enzyme/ -[enzyme-chai]: https://github.com/producthunt/chai-enzyme -[jsdom]: https://github.com/tmpvar/jsdom diff --git a/packages/compass-database/package.json b/packages/compass-database/package.json deleted file mode 100644 index 2d648f80677..00000000000 --- a/packages/compass-database/package.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "name": "@mongodb-js/compass-database", - "productName": "Database plugin", - "version": "3.19.1", - "description": "Compass Database Plugin", - "author": { - "name": "MongoDB Inc", - "email": "compass@mongodb.com" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/mongodb-js/compass.git" - }, - "homepage": "https://github.com/mongodb-js/compass", - "bugs": { - "url": "https://jira.mongodb.org/projects/COMPASS/issues", - "email": "compass@mongodb.com" - }, - "license": "SSPL", - "files": [ - "dist" - ], - "main": "dist/index.js", - "compass:main": "src/index.ts", - "types": "dist/src/index.d.ts", - "exports": { - "browser": "./dist/browser.js", - "require": "./dist/index.js" - }, - "compass:exports": { - ".": "./src/index.ts" - }, - "scripts": { - "bootstrap": "npm run postcompile", - "prepublishOnly": "npm run compile && compass-scripts check-exports-exist", - "compile": "npm run webpack -- --mode production", - "webpack": "webpack-compass", - "postcompile": "tsc --emitDeclarationOnly", - "analyze": "npm run webpack -- --mode production --analyze", - "typecheck": "tsc -p tsconfig-lint.json --noEmit", - "eslint": "eslint", - "prettier": "prettier", - "lint": "npm run eslint . && npm run prettier -- --check .", - "depcheck": "compass-scripts check-peer-deps && depcheck", - "check": "npm run typecheck && npm run lint && npm run depcheck", - "check-ci": "npm run check", - "test": "mocha", - "test-electron": "xvfb-maybe electron-mocha --no-sandbox", - "test-cov": "nyc --compact=false --produce-source-map=false -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", - "test-watch": "npm run test -- --watch", - "test-ci": "npm run test-cov", - "test-ci-electron": "npm run test-electron", - "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." - }, - "peerDependencies": { - "@mongodb-js/compass-components": "^1.19.0", - "@mongodb-js/compass-logging": "^1.2.6", - "hadron-app-registry": "^9.0.14", - "react": "^17.0.2" - }, - "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.0.11", - "@mongodb-js/mocha-config-compass": "^1.3.2", - "@mongodb-js/prettier-config-compass": "^1.0.1", - "@mongodb-js/tsconfig-compass": "^1.0.3", - "@mongodb-js/webpack-config-compass": "^1.2.5", - "@testing-library/react": "^12.1.4", - "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", - "@types/react": "^17.0.5", - "@types/react-dom": "^17.0.10", - "chai": "^4.1.2", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "mocha": "^10.2.0", - "nyc": "^15.1.0", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "dependencies": { - "@mongodb-js/compass-components": "^1.19.0", - "@mongodb-js/compass-logging": "^1.2.6", - "hadron-app-registry": "^9.0.14" - } -} diff --git a/packages/compass-database/src/components/database-tabs-provider.tsx b/packages/compass-database/src/components/database-tabs-provider.tsx deleted file mode 100644 index 211c4becbed..00000000000 --- a/packages/compass-database/src/components/database-tabs-provider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useContext, useMemo, useRef } from 'react'; - -export type DatabaseTab = { - name: string; - component: React.ComponentType; -}; - -const DatabaseTabsContext = React.createContext([]); - -export const DatabaseTabsProvider: React.FunctionComponent<{ - tabs: DatabaseTab[]; -}> = ({ tabs, children }) => { - const tabsRef = useRef(tabs); - return ( - - {children} - - ); -}; - -export function useDatabaseTabs( - filterFn: (tab: DatabaseTab) => boolean = () => true -): DatabaseTab[] { - const tabs = useContext(DatabaseTabsContext); - const filteredTabs = useMemo(() => { - return tabs.filter(filterFn); - }, [tabs, filterFn]); - return filteredTabs; -} diff --git a/packages/compass-database/src/components/database.spec.tsx b/packages/compass-database/src/components/database.spec.tsx deleted file mode 100644 index 1130e0dc32e..00000000000 --- a/packages/compass-database/src/components/database.spec.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { expect } from 'chai'; - -import { Database } from './database'; -import { DatabaseTabsProvider } from './database-tabs-provider'; - -class Collections extends React.Component { - render() { - return
Testing
; - } -} - -const ROLE = { - name: 'Collections', - component: Collections, -}; - -describe('Database [Component]', function () { - let globalBefore: any; - beforeEach(function () { - render( - - - - ); - }); - - afterEach(function () { - (global as any).hadronApp = globalBefore; - }); - - it('renders the correct roles', function () { - expect(screen.getByText('Testing')).to.be.visible; - }); -}); diff --git a/packages/compass-database/src/components/database.tsx b/packages/compass-database/src/components/database.tsx deleted file mode 100644 index 9bbcc02cfd8..00000000000 --- a/packages/compass-database/src/components/database.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { ErrorBoundary, TabNavBar, css } from '@mongodb-js/compass-components'; -import { useLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; -import { useDatabaseTabs } from './database-tabs-provider'; - -const databaseStyles = css({ - display: 'flex', - flexDirection: 'column', - alignItems: 'stretch', - height: '100%', -}); - -export function Database() { - const { log, mongoLogId } = useLoggerAndTelemetry('COMPASS-DATABASES'); - const [activeTab, setActiveTab] = useState(0); - - const onTabClicked = useCallback( - (index: number) => { - if (activeTab === index) { - return; - } - setActiveTab(index); - }, - [activeTab] - ); - - const tabs = useDatabaseTabs(); - - const tabNames = useMemo(() => tabs.map((tab) => tab.name), [tabs]); - const views = useMemo( - () => - tabs.map((tab, i) => ( - { - log.error( - mongoLogId(1001000109), - 'Database Workspace', - 'Rendering database tab failed', - { name: tab.name, error: error.message, errorInfo } - ); - }} - > - - - )), - [tabs, log, mongoLogId] - ); - - return ( -
- -
- ); -} diff --git a/packages/compass-database/src/index.ts b/packages/compass-database/src/index.ts deleted file mode 100644 index 2d49abc3bf3..00000000000 --- a/packages/compass-database/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; -import { registerHadronPlugin } from 'hadron-app-registry'; -import { DatabasePlugin, onActivated } from './plugin'; - -function activate(): void { - // noop -} - -function deactivate(): void { - // noop -} - -export const CompassDatabasePlugin = registerHadronPlugin< - object, - { logger: () => LoggerAndTelemetry } ->({ - name: 'CompassDatabase', - component: DatabasePlugin as React.FunctionComponent, - activate: onActivated, -}); - -export { DatabaseTabsProvider } from './components/database-tabs-provider'; -export { activate, deactivate }; -export { default as metadata } from '../package.json'; diff --git a/packages/compass-database/src/plugin.tsx b/packages/compass-database/src/plugin.tsx deleted file mode 100644 index f508d07b65b..00000000000 --- a/packages/compass-database/src/plugin.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Database } from './components/database'; - -export function DatabasePlugin() { - return ; -} - -export function onActivated() { - return { - store: { - state: {}, - }, - deactivate() { - /* nothing to do */ - }, - }; -} diff --git a/packages/compass-database/tsconfig.json b/packages/compass-database/tsconfig.json deleted file mode 100644 index e45df8e2f65..00000000000 --- a/packages/compass-database/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@mongodb-js/tsconfig-compass/tsconfig.react.json", - "compilerOptions": { - "outDir": "dist", - "allowJs": true - }, - "include": ["src/**/*"], - "exclude": ["./src/**/*.spec.*"] -} diff --git a/packages/compass-e2e-tests/helpers/commands/close-workspace-tabs.ts b/packages/compass-e2e-tests/helpers/commands/close-workspace-tabs.ts index 11f92c1d351..3dce959b54c 100644 --- a/packages/compass-e2e-tests/helpers/commands/close-workspace-tabs.ts +++ b/packages/compass-e2e-tests/helpers/commands/close-workspace-tabs.ts @@ -4,30 +4,18 @@ import * as Selectors from '../selectors'; export async function closeWorkspaceTabs( browser: CompassBrowser ): Promise { - const closeSelector = Selectors.CloseWorkspaceTab; - const countTabs = async () => { - const closeButtons = await browser.$$(closeSelector); - return closeButtons.length; + return (await browser.$$(Selectors.workspaceTab(null))).length; }; - let numTabs = await countTabs(); - while (numTabs > 0) { + while ((await countTabs()) > 0) { + const currentActiveTab = await browser.$( + Selectors.workspaceTab(null, true) + ); + await currentActiveTab.click(); await browser.waitUntil(async () => { - // Close the tab with keys as the close button focusing - // is finicky in e2e tests. - const META = process.platform === 'darwin' ? 'Meta' : 'Control'; - await browser.keys([META, 'w']); - await browser.keys([META]); // meta a second time to release it - - // Attempt to close the tab using the button. - const closeButtons = await browser.$$(closeSelector); - await closeButtons[0]?.click(); - - const tabCount = await countTabs(); - return tabCount < numTabs; + await currentActiveTab.$(Selectors.CloseWorkspaceTab).click(); + return (await currentActiveTab.isExisting()) === false; }); - - numTabs = await countTabs(); } } diff --git a/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts b/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts new file mode 100644 index 00000000000..fba52d38c37 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts @@ -0,0 +1,120 @@ +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; + +async function navigateToCollection( + browser: CompassBrowser, + dbName: string, + collectionName: string +): Promise { + const collectionSelector = Selectors.sidebarCollection( + dbName, + collectionName + ); + + // Close all the workspace tabs to get rid of all the state we + // might have accumulated. This is the only way to get back to the zero + // state of Schema, and Validation tabs without re-connecting. + await browser.closeWorkspaceTabs(); + + // search for the collection and wait for the collection to be there and visible + await browser.clickVisible(Selectors.SidebarFilterInput); + const sidebarFilterInputElement = await browser.$( + Selectors.SidebarFilterInput + ); + await sidebarFilterInputElement.clearValue(); + await sidebarFilterInputElement.setValue(collectionName); + const collectionElement = await browser.$(collectionSelector); + + await collectionElement.waitForDisplayed(); + + // click it and wait for the collection header to become visible + await browser.clickVisible(collectionSelector); + await waitUntilActiveCollectionTab(browser, dbName, collectionName); +} + +export async function navigateToCollectionTab( + browser: CompassBrowser, + dbName: string, + collectionName: string, + tabName: + | 'Documents' + | 'Aggregations' + | 'Schema' + | 'Indexes' + | 'Validation' = 'Documents' +): Promise { + await navigateToCollection(browser, dbName, collectionName); + await navigateWithinCurrentCollectionTabs(browser, tabName); +} + +export async function navigateWithinCurrentCollectionTabs( + browser: CompassBrowser, + tabName: + | 'Documents' + | 'Aggregations' + | 'Schema' + | 'Indexes' + | 'Validation' = 'Documents' +): Promise { + const tab = browser.$(Selectors.collectionSubTab(tabName)); + const selectedTab = browser.$(Selectors.collectionSubTab(tabName, true)); + + if (await selectedTab.isExisting()) { + return; + } + + // otherwise select the tab and wait for it to become selected + await browser.clickVisible(tab); + await waitUntilActiveCollectionSubTab(browser, tabName); +} + +export async function waitUntilActiveCollectionTab( + browser: CompassBrowser, + dbName: string, + collectionName: string, + tabName: + | 'Documents' + | 'Aggregations' + | 'Schema' + | 'Indexes' + | 'Validation' + | null = null +) { + await browser + .$(Selectors.collectionWorkspaceTab(`${dbName}.${collectionName}`, true)) + .waitForDisplayed(); + if (tabName) { + await waitUntilActiveCollectionSubTab(browser, tabName); + } +} + +export async function waitUntilActiveCollectionSubTab( + browser: CompassBrowser, + tabName: + | 'Documents' + | 'Aggregations' + | 'Schema' + | 'Indexes' + | 'Validation' = 'Documents' +) { + await browser.$(Selectors.collectionSubTab(tabName, true)).waitForDisplayed(); +} + +export async function getActiveTabNamespace(browser: CompassBrowser) { + const activeWorkspaceTitle = await browser + .$(Selectors.workspaceTab(null, true)) + .getAttribute('title'); + switch (activeWorkspaceTitle) { + case 'My Queries': + case 'Performance': + case 'Databases': + return null; + default: { + const [db, coll] = activeWorkspaceTitle.split(' > '); + if (!coll) { + return db; + } + return `${db}.${coll}`; + } + } +} diff --git a/packages/compass-e2e-tests/helpers/commands/database-workspaces.ts b/packages/compass-e2e-tests/helpers/commands/database-workspaces.ts new file mode 100644 index 00000000000..5f4dc663090 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/database-workspaces.ts @@ -0,0 +1,20 @@ +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; + +export async function navigateToDatabaseCollectionsTab( + browser: CompassBrowser, + dbName: string +): Promise { + await browser.navigateToInstanceTab('Databases'); + await browser.clickVisible(Selectors.databaseCardClickable(dbName)); + await waitUntilActiveDatabaseTab(browser, dbName); +} + +export async function waitUntilActiveDatabaseTab( + browser: CompassBrowser, + dbName: string +) { + await browser + .$(Selectors.databaseWorkspaceTab(dbName, true)) + .waitForDisplayed(); +} diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index 5c83bd3ed33..5d4a0f36581 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -9,10 +9,9 @@ export * from './connect-with-connection-string'; export * from './connect-with-connection-form'; export * from './disconnect'; export * from './shell-eval'; -export * from './navigate-to-instance-tab'; -export * from './navigate-to-database-tab'; -export * from './navigate-to-collection-tab'; -export * from './navigate-within-current-collection-tabs'; +export * from './instance-workspaces'; +export * from './database-workspaces'; +export * from './collection-workspaces'; export * from './run-find-operation'; export * from './focus-stage-operator'; export * from './select-stage-operator'; @@ -30,7 +29,6 @@ export * from './drop-namespace'; export * from './get-query-id'; export * from './run-find'; export * from './export-to-language'; -export * from './get-active-tab-namespace'; export * from './click-parent'; export * from './get-connect-form-state'; export * from './set-connect-form-state'; diff --git a/packages/compass-e2e-tests/helpers/commands/instance-workspaces.ts b/packages/compass-e2e-tests/helpers/commands/instance-workspaces.ts new file mode 100644 index 00000000000..40ac6a13f5e --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/instance-workspaces.ts @@ -0,0 +1,22 @@ +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; + +export async function navigateToInstanceTab( + browser: CompassBrowser, + tabName: 'My Queries' | 'Performance' | 'Databases' = 'My Queries' +): Promise { + const sidebarNavigationItem = browser.$( + Selectors.sidebarInstanceNavigationItem(tabName) + ); + await browser.clickVisible(sidebarNavigationItem); + await waitUntilActiveInstanceTab(browser, tabName); +} + +export async function waitUntilActiveInstanceTab( + browser: CompassBrowser, + tabName: 'My Queries' | 'Performance' | 'Databases' = 'My Queries' +) { + await browser + .$(Selectors.instanceWorkspaceTab(tabName, true)) + .waitForDisplayed(); +} diff --git a/packages/compass-e2e-tests/helpers/commands/navigate-to-collection-tab.ts b/packages/compass-e2e-tests/helpers/commands/navigate-to-collection-tab.ts deleted file mode 100644 index 3cf463b3c96..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/navigate-to-collection-tab.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { expect } from 'chai'; -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -async function navigateToCollection( - browser: CompassBrowser, - dbName: string, - collectionName: string -): Promise { - const headerSelector = Selectors.collectionHeaderTitle( - dbName, - collectionName - ); - const collectionSelector = Selectors.sidebarCollection( - dbName, - collectionName - ); - - const headerElement = await browser.$(headerSelector); - - // Close all the workspace tabs to get rid of all the state we - // might have accumulated. This is the only way to get back to the zero - // state of Schema, and Validation tabs without re-connecting. - await browser.closeWorkspaceTabs(); - - // search for the collection and wait for the collection to be there and visible - await browser.clickVisible(Selectors.SidebarFilterInput); - const sidebarFilterInputElement = await browser.$( - Selectors.SidebarFilterInput - ); - await sidebarFilterInputElement.clearValue(); - await sidebarFilterInputElement.setValue(collectionName); - const collectionElement = await browser.$(collectionSelector); - - await collectionElement.waitForDisplayed(); - - // click it and wait for the collection header to become visible - await browser.clickVisible(collectionSelector); - await headerElement.waitForDisplayed(); -} - -export async function navigateToCollectionTab( - browser: CompassBrowser, - dbName: string, - collectionName: string, - tabName: string -): Promise { - const tabSelector = Selectors.collectionTab(tabName); - const tabSelectedSelector = Selectors.collectionTab(tabName, true); - - await navigateToCollection(browser, dbName, collectionName); - - const tabSelectedSelectorElement = await browser.$(tabSelectedSelector); - // if the correct tab is already visible, do nothing - if (await tabSelectedSelectorElement.isExisting()) { - return; - } - - // otherwise select the tab and wait for it to become selected - await browser.clickVisible(tabSelector); - - await tabSelectedSelectorElement.waitForDisplayed(); - - // regression test: The workspace tab should contain the document tab name. - const workspaceTabText = await browser - .$(Selectors.SelectedWorkspaceTabButton) - .getText(); - // example: 'Indexestest.test' - expect(workspaceTabText).to.contain(tabName); -} diff --git a/packages/compass-e2e-tests/helpers/commands/navigate-to-database-tab.ts b/packages/compass-e2e-tests/helpers/commands/navigate-to-database-tab.ts deleted file mode 100644 index 48c0710b0fa..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/navigate-to-database-tab.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -import { expect } from 'chai'; - -export async function navigateToDatabaseTab( - browser: CompassBrowser, - dbName: string, - tabName: string -): Promise { - await browser.navigateToInstanceTab('Databases'); - - await browser.clickVisible(Selectors.databaseCardClickable(dbName)); - - // there is only the one tab for now, so this is just an assertion - expect(tabName).to.equal('Collections'); - - const tabSelectedSelector = Selectors.databaseTab(tabName, true); - - const tabSelectorElement = await browser.$(tabSelectedSelector); - await tabSelectorElement.waitForDisplayed(); -} diff --git a/packages/compass-e2e-tests/helpers/commands/navigate-to-instance-tab.ts b/packages/compass-e2e-tests/helpers/commands/navigate-to-instance-tab.ts deleted file mode 100644 index 85fd698ac8b..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/navigate-to-instance-tab.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { CompassBrowser } from '../compass-browser'; -import * as Selectors from '../selectors'; - -export async function navigateToInstanceTab( - browser: CompassBrowser, - tabName: string -): Promise { - const tabSelector = Selectors.instanceTab(tabName); - const tabSelectedSelector = Selectors.instanceTab(tabName, true); - - await browser.clickVisible(Selectors.SidebarTitle); - const instanceTabElement = await browser.$(Selectors.InstanceTabs); - await instanceTabElement.waitForDisplayed(); - - const tabSelectorElement = await browser.$(tabSelectedSelector); - - // if the correct tab is already visible, do nothing - if (await tabSelectorElement.isExisting()) { - return; - } - - // otherwise select the tab and wait for it to become selected - await browser.clickVisible(tabSelector); - await tabSelectorElement.waitForDisplayed(); -} diff --git a/packages/compass-e2e-tests/helpers/commands/navigate-within-current-collection-tabs.ts b/packages/compass-e2e-tests/helpers/commands/navigate-within-current-collection-tabs.ts deleted file mode 100644 index 7fb1ca91370..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/navigate-within-current-collection-tabs.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect } from 'chai'; -import { Selectors } from '../compass'; -import { type CompassBrowser } from '../compass-browser'; - -export async function navigateWithinCurrentCollectionTabs( - browser: CompassBrowser, - tabName: string -): Promise { - const tabSelector = Selectors.collectionTab(tabName); - const tabSelectedSelector = Selectors.collectionTab(tabName, true); - - const tabSelectedSelectorElement = await browser.$(tabSelectedSelector); - // if the correct tab is already visible, do nothing - if (await tabSelectedSelectorElement.isExisting()) { - return; - } - - // otherwise select the tab and wait for it to become selected - await browser.clickVisible(tabSelector); - - await tabSelectedSelectorElement.waitForDisplayed(); - - // regression test: The workspace tab should contain the document tab name. - const workspaceTabText = await browser - .$(Selectors.SelectedWorkspaceTabButton) - .getText(); - // example: 'Indexestest.test' - expect(workspaceTabText).to.contain(tabName); -} diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 78c70f4117e..e7537c2409f 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -389,8 +389,6 @@ export const ShellInputEditor = '[data-testid="shell-input"] [data-codemirror]'; export const ShellOutput = '[data-testid="shell-output"]'; // Instance screen -export const InstanceTabs = '[data-testid="instance-tabs"]'; -export const InstanceTab = '.test-tab-nav-bar-tab'; export const DatabasesTable = '[data-testid="database-grid"]'; export const InstanceCreateDatabaseButton = '[data-testid="database-grid"] [data-testid="create-controls"] button'; @@ -402,19 +400,6 @@ export const DatabaseCardDrop = '[data-testid="database-grid"] [data-testid="namespace-card-actions"] button'; export const ServerStats = '.serverstats'; -export const instanceTab = (tabName: string, selected?: boolean): string => { - const selector = `${InstanceTab}[name="${tabName}"]`; - - if (selected === true) { - return `${selector}[aria-selected="true"]`; - } - - if (selected === false) { - return `${selector}[aria-selected="false"]`; - } - - return selector; -}; export const databaseCard = (dbName: string): string => { return `${DatabaseCard}[data-id="${dbName}"]`; }; @@ -426,8 +411,6 @@ export const databaseCardClickable = (dbName: string): string => { }; // Database screen -export const DatabaseTabs = '[data-testid="database-tabs"]'; -export const DatabaseTab = '.test-tab-nav-bar-tab'; export const CollectionsGrid = '[data-testid="collection-grid"]'; export const DatabaseCreateCollectionButton = '[data-testid="collection-grid"] [data-testid="create-controls"] button'; @@ -438,20 +421,6 @@ export const CollectionCard = '[data-testid="collection-grid-item"]'; export const CollectionCardDrop = '[data-testid="collection-grid"] [data-testid="namespace-card-actions"] button'; -export const databaseTab = (tabName: string, selected?: boolean): string => { - const selector = `${DatabaseTab}[name="${tabName}"]`; - - if (selected === true) { - return `${selector}[aria-selected="true"]`; - } - - if (selected === false) { - return `${selector}[aria-selected="false"]`; - } - - return selector; -}; - export const collectionCard = ( dbName: string, collectionName: string @@ -472,7 +441,7 @@ export const collectionCardClickable = ( }; // Collection screen -export const CollectionTab = '.test-tab-nav-bar-tab'; +export const CollectionTab = '[data-testid="collection-tabs"]'; export const CollectionHeaderTitle = '[data-testid="collection-header-title"]'; export const CollectionHeaderNamespace = '[data-testid="collection-header-namespace"]'; @@ -491,8 +460,11 @@ export const TooltipIndexesTotalSize = '[data-testid="tooltip-indexes-total-size"]'; export const TooltipIndexesAvgSize = '[data-testid="tooltip-indexes-avg-size"]'; -export const collectionTab = (tabName: string, selected?: boolean): string => { - const selector = `${CollectionTab}[name="${tabName}"]`; +export const collectionSubTab = ( + tabName: string, + selected?: boolean +): string => { + const selector = `${CollectionTab} [name="${tabName}"]`; if (selected === true) { return `${selector}[aria-selected="true"]`; @@ -721,7 +693,7 @@ export const QueryHistoryFavoritesButton = `[data-testid="past-queries-favorites export const QueryHistoryFavoriteItem = `[data-testid="favorite-query-list-item"]`; export const myQueriesItem = (title: string): string => { - return `[data-testid="my-queries-content"] [title="${title}"]`; + return `[data-testid="my-queries-list"] [title="${title}"]`; }; export const MyQueriesList = '[data-testid="my-queries-list"]'; @@ -1131,10 +1103,40 @@ export const QueryBarAIGenerateQueryButton = '[data-testid="ai-generate-button"]'; export const QueryBarAIErrorMessageBanner = '[data-testid="ai-error-msg"]'; -// Workspace tabs at the top -export const SelectedWorkspaceTabButton = - '[data-testid=workspace-tab-button][aria-selected="true"]'; +// Workspace tabs export const CloseWorkspaceTab = '[data-testid="close-workspace-tab"]'; +export const sidebarInstanceNavigationItem = ( + tabName: 'My Queries' | 'Performance' | 'Databases' = 'My Queries' +) => { + return `[data-testid="navigation-sidebar"] [aria-label="${tabName}"]`; +}; +export const workspaceTab = ( + title: string | null, + active: boolean | null = null +) => { + const _active = active === null ? '' : `[aria-selected="${String(active)}"]`; + const _title = title === null ? '' : `[title="${title}"]`; + return `[role="tablist"][aria-label="Workspace Tabs"] [role="tab"]${_title}${_active}`; +}; +export const instanceWorkspaceTab = ( + tabName: 'My Queries' | 'Performance' | 'Databases' = 'My Queries', + active: boolean | null = null +) => { + return workspaceTab(tabName, active); +}; +export const databaseWorkspaceTab = ( + dbName: string, + active: boolean | null = null +) => { + return workspaceTab(dbName, active); +}; +export const collectionWorkspaceTab = ( + namespace: string, + active: boolean | null = null +) => { + const [db, ...coll] = namespace.split('.'); + return workspaceTab(`${db} > ${coll.join('.')}`, active); +}; // Export modal export const ExportModal = '[data-testid="export-modal"]'; diff --git a/packages/compass-e2e-tests/package.json b/packages/compass-e2e-tests/package.json index fb3a55f10c9..271105a79cc 100644 --- a/packages/compass-e2e-tests/package.json +++ b/packages/compass-e2e-tests/package.json @@ -27,7 +27,7 @@ "server-info": "ts-node ./scripts/server-info.ts" }, "devDependencies": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@mongodb-js/compass-test-server": "^0.1.6", "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/oidc-mock-provider": "^0.4.1", @@ -46,7 +46,7 @@ "cross-spawn": "^7.0.3", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "fast-glob": "^3.2.7", "glob": "^10.2.5", diff --git a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts index cc31bd864cc..514845979a3 100644 --- a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts @@ -65,8 +65,8 @@ async function getRecentQueries( } async function navigateToTab(browser: CompassBrowser, tabName: string) { - const tabSelector = Selectors.collectionTab(tabName); - const tabSelectedSelector = Selectors.collectionTab(tabName, true); + const tabSelector = Selectors.collectionSubTab(tabName); + const tabSelectedSelector = Selectors.collectionSubTab(tabName, true); const tabSelectedSelectorElement = await browser.$(tabSelectedSelector); // if the correct tab is already visible, do nothing diff --git a/packages/compass-e2e-tests/tests/collection-heading.test.ts b/packages/compass-e2e-tests/tests/collection-heading.test.ts index 364a7d758a9..f264c94e857 100644 --- a/packages/compass-e2e-tests/tests/collection-heading.test.ts +++ b/packages/compass-e2e-tests/tests/collection-heading.test.ts @@ -38,7 +38,7 @@ describe('Collection heading', function () { 'Schema', 'Indexes', 'Validation', - ].map((selector) => Selectors.collectionTab(selector)); + ].map((selector) => Selectors.collectionSubTab(selector)); for (const tabSelector of tabSelectors) { const tabElement = await browser.$(tabSelector); diff --git a/packages/compass-e2e-tests/tests/database-collections-tab.test.ts b/packages/compass-e2e-tests/tests/database-collections-tab.test.ts index 5221a5454b7..0c987e6d47d 100644 --- a/packages/compass-e2e-tests/tests/database-collections-tab.test.ts +++ b/packages/compass-e2e-tests/tests/database-collections-tab.test.ts @@ -30,7 +30,7 @@ describe('Database collections tab', function () { await createDummyCollections(); await createNumbersCollection(); await browser.connectWithConnectionString(); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); }); afterEach(async function () { @@ -79,7 +79,7 @@ describe('Database collections tab', function () { 'Schema', 'Indexes', 'Validation', - ].map((selector) => Selectors.collectionTab(selector)); + ].map((selector) => Selectors.collectionSubTab(selector)); for (const tabSelector of tabSelectors) { const tabElement = await browser.$(tabSelector); @@ -99,7 +99,7 @@ describe('Database collections tab', function () { 'add-collection-modal-basic.png' ); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); const selector = Selectors.collectionCard('test', collectionName); await browser.scrollToVirtualItem( @@ -135,9 +135,7 @@ describe('Database collections tab', function () { // the app should still be on the database Collections tab because there are // other collections in this database - await browser - .$(Selectors.databaseTab('Collections', true)) - .waitForDisplayed(); + await browser.waitUntilActiveDatabaseTab('test'); }); it('can create a capped collection', async function () { @@ -156,7 +154,7 @@ describe('Database collections tab', function () { 'add-collection-modal-capped.png' ); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); const selector = Selectors.collectionCard('test', collectionName); await browser.scrollToVirtualItem( @@ -194,7 +192,7 @@ describe('Database collections tab', function () { 'add-collection-modal-custom-collation.png' ); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); const selector = Selectors.collectionCard('test', collectionName); await browser.scrollToVirtualItem( @@ -233,7 +231,7 @@ describe('Database collections tab', function () { 'add-collection-modal-timeseries.png' ); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); const selector = Selectors.collectionCard('test', collectionName); await browser.scrollToVirtualItem( @@ -273,7 +271,7 @@ describe('Database collections tab', function () { 'add-collection-modal-timeseries.png' ); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); const selector = Selectors.collectionCard('test', collectionName); await browser.scrollToVirtualItem( @@ -311,7 +309,7 @@ describe('Database collections tab', function () { 'add-collection-modal-clustered.png' ); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); const selector = Selectors.collectionCard('test', collectionName); await browser.scrollToVirtualItem( @@ -342,7 +340,7 @@ describe('Database collections tab', function () { // Create the collection and refresh await browser.shellEval(`use ${db};`); await browser.shellEval(`db.createCollection('${coll}');`); - await browser.navigateToDatabaseTab(db, 'Collections'); + await browser.navigateToDatabaseCollectionsTab(db); await browser.clickVisible(Selectors.DatabaseRefreshCollectionButton); const collSelector = Selectors.collectionCard(db, coll); diff --git a/packages/compass-e2e-tests/tests/in-use-encryption.test.ts b/packages/compass-e2e-tests/tests/in-use-encryption.test.ts index 925d10f2c86..9ac84440677 100644 --- a/packages/compass-e2e-tests/tests/in-use-encryption.test.ts +++ b/packages/compass-e2e-tests/tests/in-use-encryption.test.ts @@ -185,7 +185,7 @@ describe('CSFLE / QE', function () { }); it('can create a fle2 collection with encryptedFields', async function () { - await browser.navigateToDatabaseTab(databaseName, 'Collections'); + await browser.navigateToDatabaseCollectionsTab(databaseName); // open the create collection modal from the button at the top await browser.clickVisible(Selectors.DatabaseCreateCollectionButton); @@ -204,7 +204,7 @@ describe('CSFLE / QE', function () { 'add-collection-modal-encryptedfields.png' ); - await browser.navigateToDatabaseTab(databaseName, 'Collections'); + await browser.navigateToDatabaseCollectionsTab(databaseName); const collectionListFLE2BadgeElement = await browser.$( Selectors.CollectionListFLE2Badge @@ -320,11 +320,11 @@ describe('CSFLE / QE', function () { }); it('can create a fle2 collection without encryptedFields', async function () { - await browser.navigateToDatabaseTab(databaseName, 'Collections'); + await browser.navigateToDatabaseCollectionsTab(databaseName); await browser.clickVisible(Selectors.DatabaseCreateCollectionButton); await browser.addCollection(collectionName); - await browser.navigateToDatabaseTab(databaseName, 'Collections'); + await browser.navigateToDatabaseCollectionsTab(databaseName); const selector = Selectors.collectionCard(databaseName, collectionName); await browser.scrollToVirtualItem( diff --git a/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts b/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts index ef42dca602a..29f68979509 100644 --- a/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts +++ b/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts @@ -130,8 +130,7 @@ describe('Instance databases tab', function () { await databaseCard.waitForExist({ reverse: true }); // the app should stay on the instance Databases tab. - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - await browser.$(tabSelectedSelector).waitForDisplayed(); + await browser.waitUntilActiveInstanceTab('Databases'); }); it('can refresh the list of databases using refresh controls', async function () { diff --git a/packages/compass-e2e-tests/tests/instance-sidebar.test.ts b/packages/compass-e2e-tests/tests/instance-sidebar.test.ts index ff314608b1c..93a03247a37 100644 --- a/packages/compass-e2e-tests/tests/instance-sidebar.test.ts +++ b/packages/compass-e2e-tests/tests/instance-sidebar.test.ts @@ -134,7 +134,7 @@ describe('Instance sidebar', function () { ); await browser.$(headerSelector).waitForDisplayed(); await browser - .$(Selectors.collectionTab('Documents', true)) + .$(Selectors.collectionSubTab('Documents', true)) .waitForDisplayed(); await browser.clickVisible(Selectors.sidebarDatabase(dbName)); @@ -146,12 +146,6 @@ describe('Instance sidebar', function () { await collectionElement.waitForDisplayed(); await browser.dropDatabaseFromSidebar(dbName); - - // the app should land back on the instance Databases tab because it was - // still on the collection Documents tab - await browser - .$(Selectors.instanceTab('Databases', true)) - .waitForDisplayed(); }); it('can create a collection and drop it', async function () { @@ -179,17 +173,10 @@ describe('Instance sidebar', function () { collectionName ); await browser.$(headerSelector).waitForDisplayed(); - const tabSelectedSelector = Selectors.collectionTab('Documents', true); + const tabSelectedSelector = Selectors.collectionSubTab('Documents', true); await browser.$(tabSelectedSelector).waitForDisplayed(); await browser.dropCollectionFromSidebar(dbName, collectionName); - - // the app should have redirected to the the database Collections tab - // because we were on the collection Documents tab and the database has - // other collections - await browser - .$(Selectors.databaseTab('Collections', true)) - .waitForDisplayed(); }); it('can refresh the databases', async function () { diff --git a/packages/compass-e2e-tests/tests/oidc.test.ts b/packages/compass-e2e-tests/tests/oidc.test.ts index f16e0476051..1b3aaa6baa8 100644 --- a/packages/compass-e2e-tests/tests/oidc.test.ts +++ b/packages/compass-e2e-tests/tests/oidc.test.ts @@ -308,7 +308,9 @@ describe('OIDC integration', function () { afterReauth = true; await browser.clickVisible(`${modal} ${cancelButton}`); - const errorBanner = await browser.$('[role=alert]'); + const errorBanner = await browser.$( + '[data-testid="toast-instance-refresh-failed"]' + ); await errorBanner.waitForDisplayed(); expect(await errorBanner.getText()).to.include( 'Reauthentication declined by user' diff --git a/packages/compass-e2e-tests/tests/read-only.test.ts b/packages/compass-e2e-tests/tests/read-only.test.ts index 17379583e2b..38cd0cc8918 100644 --- a/packages/compass-e2e-tests/tests/read-only.test.ts +++ b/packages/compass-e2e-tests/tests/read-only.test.ts @@ -158,7 +158,7 @@ describe('readOnly: true / Read-Only Edition', function () { await createNumbersCollection(); await browser.connectWithConnectionString(); - await browser.navigateToDatabaseTab('test', 'Collections'); + await browser.navigateToDatabaseCollectionsTab('test'); let databaseCreateCollectionButton = await browser.$( Selectors.DatabaseCreateCollectionButton diff --git a/packages/compass-e2e-tests/tests/redirect.test.ts b/packages/compass-e2e-tests/tests/redirect.test.ts deleted file mode 100644 index 013793110a0..00000000000 --- a/packages/compass-e2e-tests/tests/redirect.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import chai from 'chai'; -import type { CompassBrowser } from '../helpers/compass-browser'; -import { beforeTests, afterTests, afterTest } from '../helpers/compass'; -import type { Compass } from '../helpers/compass'; -import * as Selectors from '../helpers/selectors'; -import { - createNumbersCollection, - createMultipleCollections, -} from '../helpers/insert-data'; - -const { expect } = chai; - -describe('redirects', function () { - let compass: Compass; - let browser: CompassBrowser; - - before(async function () { - compass = await beforeTests(); - browser = compass.browser; - }); - - beforeEach(async function () { - await createNumbersCollection(); - await createMultipleCollections(); - await browser.connectWithConnectionString(); - }); - - after(async function () { - await afterTests(compass, this.currentTest); - }); - - afterEach(async function () { - await afterTest(compass, this.currentTest); - }); - - // on a collection tab - it('redirects to Collections if on the collection being removed and there are other collections in that database', async function () { - await browser.navigateToCollectionTab( - 'multiple-collections', - 'one', - 'Documents' - ); - await browser.dropCollectionFromSidebar('multiple-collections', 'one'); - - const tabSelectedSelector = Selectors.databaseTab('Collections', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - await tabSelectorElement.waitForDisplayed(); - }); - - it('redirects to Databases if on the collection being removed and it was the only collection in that database', async function () { - await browser.navigateToCollectionTab('test', 'numbers', 'Documents'); - await browser.dropCollectionFromSidebar('test', 'numbers'); - - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - await tabSelectorElement.waitForDisplayed(); - }); - - it('does nothing if on a different collection to the one being removed', async function () { - await browser.navigateToCollectionTab( - 'multiple-collections', - 'one', - 'Documents' - ); - await browser.dropCollectionFromSidebar('multiple-collections', 'two'); - - // still on a Collection's Documents tab, so presumably didn't get redirected - const tabSelectedSelector = Selectors.collectionTab('Documents', true); - const tabSelectedElement = await browser.$(tabSelectedSelector); - expect(await tabSelectedElement.isDisplayed()).to.be.true; - }); - - it('redirects to Databases if on a collection for the database being removed', async function () { - await browser.navigateToCollectionTab('test', 'numbers', 'Documents'); - await browser.dropDatabaseFromSidebar('test'); - - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - await tabSelectorElement.waitForDisplayed(); - }); - - it('does nothing if on a collection for a different database being removed', async function () { - await browser.navigateToCollectionTab( - 'multiple-collections', - 'one', - 'Documents' - ); - await browser.dropDatabaseFromSidebar('test'); - - // still on a Collection's Documents tab, so presumably didn't get redirected - const tabSelectedSelector = Selectors.collectionTab('Documents', true); - const tabSelectedElement = await browser.$(tabSelectedSelector); - expect(await tabSelectedElement.isDisplayed()).to.be.true; - }); - - // on the Collections list - it('does nothing if on Collections list containing the collection being removed and there are other collections in that database', async function () { - await browser.navigateToDatabaseTab('multiple-collections', 'Collections'); - await browser.dropCollectionFromSidebar('multiple-collections', 'two'); - - // still on a Database's Collections tab, so presumably didn't get redirected - const tabSelectedSelector = Selectors.databaseTab('Collections', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - it('redirects to Databases if on Collections list containing the collection being removed and it was the only collection in the database', async function () { - await browser.navigateToDatabaseTab('test', 'Collections'); - await browser.dropCollectionFromSidebar('test', 'numbers'); - - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - await tabSelectorElement.waitForDisplayed(); - }); - - it('redirects to Databases if on Collections list for the database being removed.', async function () { - await browser.navigateToDatabaseTab('test', 'Collections'); - await browser.dropDatabaseFromSidebar('test'); - - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - await tabSelectorElement.waitForDisplayed(); - }); - - it('does nothing if on Collections list for a different database to the one being removed', async function () { - await browser.navigateToDatabaseTab('multiple-collections', 'Collections'); - await browser.dropDatabaseFromSidebar('test'); - - // still on a Database's Collections tab, so presumably didn't get redirected - const tabSelectedSelector = Selectors.databaseTab('Collections', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - // on any other part of the app - it('does nothing if on Databases and a collection gets removed', async function () { - await browser.navigateToInstanceTab('Databases'); - await browser.dropCollectionFromSidebar('test', 'numbers'); - - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - it('does nothing if on Databases and a database gets removed', async function () { - await browser.navigateToInstanceTab('Databases'); - await browser.dropDatabaseFromSidebar('test'); - - const tabSelectedSelector = Selectors.instanceTab('Databases', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - it('does nothing if on My Queries and a collection gets removed', async function () { - await browser.navigateToInstanceTab('My Queries'); - await browser.dropCollectionFromSidebar('test', 'numbers'); - - const tabSelectedSelector = Selectors.instanceTab('My Queries', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - it('does nothing if on My Queries and a database gets removed', async function () { - await browser.navigateToInstanceTab('My Queries'); - await browser.dropDatabaseFromSidebar('test'); - - const tabSelectedSelector = Selectors.instanceTab('My Queries', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - it('does nothing if on Performance and a database gets removed', async function () { - await browser.navigateToInstanceTab('Performance'); - await browser.dropCollectionFromSidebar('test', 'numbers'); - - const tabSelectedSelector = Selectors.instanceTab('Performance', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); - - it('does nothing if on Performance and a collection gets removed', async function () { - await browser.navigateToInstanceTab('Performance'); - await browser.dropDatabaseFromSidebar('test'); - - const tabSelectedSelector = Selectors.instanceTab('Performance', true); - const tabSelectorElement = await browser.$(tabSelectedSelector); - expect(await tabSelectorElement.isDisplayed()).to.be.true; - }); -}); diff --git a/packages/compass-e2e-tests/tests/search-indexes.test.ts b/packages/compass-e2e-tests/tests/search-indexes.test.ts index 46c4ee9e741..c3756b28a76 100644 --- a/packages/compass-e2e-tests/tests/search-indexes.test.ts +++ b/packages/compass-e2e-tests/tests/search-indexes.test.ts @@ -325,10 +325,7 @@ describe('Search Indexes', function () { const namespace = await browser.getActiveTabNamespace(); expect(namespace).to.equal(`${DB_NAME}.${collectionName}`); - const workspaceTabText = await browser - .$(Selectors.SelectedWorkspaceTabButton) - .getText(); - expect(workspaceTabText).to.contain('Aggregations'); + await browser.waitUntilActiveCollectionSubTab('Aggregations'); }); }); } diff --git a/packages/compass-explain-plan/package.json b/packages/compass-explain-plan/package.json index 95573fc02f2..3898f0b6c41 100644 --- a/packages/compass-explain-plan/package.json +++ b/packages/compass-explain-plan/package.json @@ -78,7 +78,7 @@ "d3-flextree": "2.1.2", "d3-hierarchy": "^3.1.2", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "lodash": "^4.17.21", diff --git a/packages/compass-find-in-page/package.json b/packages/compass-find-in-page/package.json index 3e5cbd4251c..7d5c7579769 100644 --- a/packages/compass-find-in-page/package.json +++ b/packages/compass-find-in-page/package.json @@ -77,7 +77,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "mocha": "^10.2.0", "nyc": "^15.1.0", diff --git a/packages/compass-home/package.json b/packages/compass-home/package.json index ea8d0065975..d1d753c64e8 100644 --- a/packages/compass-home/package.json +++ b/packages/compass-home/package.json @@ -41,11 +41,9 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-find-in-page": "^4.19.1", "@mongodb-js/compass-import-export": "^7.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", "@mongodb-js/compass-schema-validation": "^6.20.0", @@ -54,6 +52,7 @@ "@mongodb-js/compass-shell": "^3.19.1", "@mongodb-js/compass-sidebar": "^5.19.1", "@mongodb-js/compass-welcome": "^0.18.1", + "@mongodb-js/compass-workspaces": "^0.1.0", "@mongodb-js/connection-storage": "^0.6.6", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", @@ -68,11 +67,9 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-find-in-page": "^4.19.1", "@mongodb-js/compass-import-export": "^7.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", "@mongodb-js/compass-schema-validation": "^6.20.0", @@ -81,6 +78,7 @@ "@mongodb-js/compass-shell": "^3.19.1", "@mongodb-js/compass-sidebar": "^5.19.1", "@mongodb-js/compass-welcome": "^0.18.1", + "@mongodb-js/compass-workspaces": "^0.1.0", "@mongodb-js/connection-storage": "^0.6.6", "compass-preferences-model": "^2.15.6", "hadron-app-registry": "^9.0.14", diff --git a/packages/compass-home/src/components/workspace-content.spec.tsx b/packages/compass-home/src/components/workspace-content.spec.tsx deleted file mode 100644 index 836bba2f63f..00000000000 --- a/packages/compass-home/src/components/workspace-content.spec.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { cleanup, render, screen } from '@testing-library/react'; -import { expect } from 'chai'; -import { globalAppRegistry, AppRegistryProvider } from 'hadron-app-registry'; -import { MongoDBInstanceProvider } from '@mongodb-js/compass-app-stores/provider'; -import WorkspaceContent from './workspace-content'; - -const getComponent = (name: string) => { - class TestComponent extends React.Component { - render() { - return React.createElement( - 'div', - { - 'data-testid': `test-${name}`, - }, - name - ); - } - } - return TestComponent; -}; - -function renderWorkspaceContent( - props: React.ComponentProps -) { - return render( - - - - - - ); -} - -describe('WorkspaceContent [Component]', function () { - before(function () { - ['Collection.Workspace'].map((name) => - globalAppRegistry.registerComponent(name, getComponent(name)) - ); - globalAppRegistry.onActivated(); - }); - - afterEach(cleanup); - - describe('namespace is unset', function () { - beforeEach(function () { - renderWorkspaceContent({ namespace: { database: '', collection: '' } }); - }); - - it('renders content correctly', function () { - expect(screen.queryByTestId('test-Collection.Workspace')).to.not.exist; - }); - }); - - describe('namespace has a db', function () { - beforeEach(function () { - renderWorkspaceContent({ namespace: { database: 'db', collection: '' } }); - }); - - it('renders content correctly', function () { - expect(screen.queryByTestId('test-Collection.Workspace')).to.not.exist; - }); - }); - - describe('namespace has db and collection', function () { - beforeEach(function () { - renderWorkspaceContent({ - namespace: { database: 'db', collection: 'col' }, - }); - }); - - it('renders content correctly', function () { - expect(screen.getByTestId('test-Collection.Workspace')).to.be.visible; - }); - }); -}); diff --git a/packages/compass-home/src/components/workspace-content.tsx b/packages/compass-home/src/components/workspace-content.tsx index 94fe749bd49..931e8361890 100644 --- a/packages/compass-home/src/components/workspace-content.tsx +++ b/packages/compass-home/src/components/workspace-content.tsx @@ -1,37 +1,39 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { - useAppRegistryComponent, - useAppRegistryRole, -} from 'hadron-app-registry'; -import InstanceWorkspacePlugin, { - InstanceTabsProvider, -} from '@mongodb-js/compass-instance'; -import { - CompassDatabasePlugin, - DatabaseTabsProvider, -} from '@mongodb-js/compass-database'; import { CompassSchemaValidationPlugin } from '@mongodb-js/compass-schema-validation'; -import CompassSavedAggregationsQueriesPlugin from '@mongodb-js/compass-saved-aggregations-queries'; -import { InstanceTab as DatabasesTabPlugin } from '@mongodb-js/compass-databases-collections'; -import { InstanceTab as PerformanceTabPlugin } from '@mongodb-js/compass-serverstats'; import type Namespace from '../types/namespace'; -import { CollectionTabsProvider } from '@mongodb-js/compass-collection'; +import { + WorkspaceTab as CollectionWorkspace, + CollectionTabsProvider, +} from '@mongodb-js/compass-collection'; import { CompassAggregationsPlugin } from '@mongodb-js/compass-aggregations'; +import WorkspacesPlugin, { + WorkspacesProvider, +} from '@mongodb-js/compass-workspaces'; +import { WorkspaceTab as MyQueriesWorkspace } from '@mongodb-js/compass-saved-aggregations-queries'; +import { WorkspaceTab as PerformanceWorkspace } from '@mongodb-js/compass-serverstats'; +import { + DatabasesWorkspaceTab, + CollectionsWorkspaceTab, +} from '@mongodb-js/compass-databases-collections'; import { CompassDocumentsPlugin } from '@mongodb-js/compass-crud'; -const EmptyComponent: React.FunctionComponent = () => null; - const WorkspaceContent: React.FunctionComponent<{ namespace: Namespace }> = ({ - namespace, + // TODO: clean-up, this state is not needed here anymore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + namespace: _namespace, }) => { - const databaseTabs = useAppRegistryRole('Database.Tab'); - - const Collection = - useAppRegistryComponent('Collection.Workspace') ?? EmptyComponent; - - if (namespace.collection) { - return ( + return ( + = ({ CompassDocumentsPlugin, ]} > - + - ); - } - - if (namespace.database) { - return ( - - - - ); - } - - return ( - - - + ); }; diff --git a/packages/compass-import-export/package.json b/packages/compass-import-export/package.json index b2971580225..c817c003655 100644 --- a/packages/compass-import-export/package.json +++ b/packages/compass-import-export/package.json @@ -62,7 +62,7 @@ "@mongodb-js/compass-utils": "^0.5.5", "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-app-registry": "^9.0.14", "hadron-document": "^8.4.3", "mongodb-data-service": "^22.15.1", @@ -75,7 +75,7 @@ "@mongodb-js/compass-utils": "^0.5.5", "bson": "^6.2.0", "compass-preferences-model": "^2.15.6", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-app-registry": "^9.0.14", "hadron-document": "^8.4.3", "mongodb-data-service": "^22.15.1" diff --git a/packages/compass-indexes/package.json b/packages/compass-indexes/package.json index e62403f87c2..9d0ea8b98be 100644 --- a/packages/compass-indexes/package.json +++ b/packages/compass-indexes/package.json @@ -76,7 +76,7 @@ "chai": "^4.2.0", "depcheck": "^1.4.1", "ejson-shell-parser": "^2.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", diff --git a/packages/compass-instance/.depcheckrc b/packages/compass-instance/.depcheckrc deleted file mode 100644 index 04961da4d37..00000000000 --- a/packages/compass-instance/.depcheckrc +++ /dev/null @@ -1,9 +0,0 @@ -ignores: - - '@mongodb-js/prettier-config-compass' - - '@mongodb-js/tsconfig-compass' - - '@mongodb-js/tsconfig-compass' - - '@types/react' - - '@types/react-dom' - - '@types/chai' -ignore-patterns: - - 'dist' diff --git a/packages/compass-instance/.eslintignore b/packages/compass-instance/.eslintignore deleted file mode 100644 index 85a8a75e68c..00000000000 --- a/packages/compass-instance/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -.nyc-output -dist diff --git a/packages/compass-instance/.eslintrc.js b/packages/compass-instance/.eslintrc.js deleted file mode 100644 index f4285e89285..00000000000 --- a/packages/compass-instance/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - root: true, - extends: ['@mongodb-js/eslint-config-compass'], - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig-lint.json'], - }, - env: { - node: true, - browser: true, - }, -}; diff --git a/packages/compass-instance/.mocharc.js b/packages/compass-instance/.mocharc.js deleted file mode 100644 index a7e53abc444..00000000000 --- a/packages/compass-instance/.mocharc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@mongodb-js/mocha-config-compass/compass-plugin'); diff --git a/packages/compass-instance/.prettierignore b/packages/compass-instance/.prettierignore deleted file mode 100644 index ec0e36b246c..00000000000 --- a/packages/compass-instance/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -.nyc_output -dist -coverage \ No newline at end of file diff --git a/packages/compass-instance/.prettierrc.json b/packages/compass-instance/.prettierrc.json deleted file mode 100644 index 18853d1532e..00000000000 --- a/packages/compass-instance/.prettierrc.json +++ /dev/null @@ -1 +0,0 @@ -"@mongodb-js/prettier-config-compass" diff --git a/packages/compass-instance/README.md b/packages/compass-instance/README.md deleted file mode 100644 index 45c16345888..00000000000 --- a/packages/compass-instance/README.md +++ /dev/null @@ -1 +0,0 @@ -# Compass instance plugin diff --git a/packages/compass-instance/src/components/instance-tabs-provider.tsx b/packages/compass-instance/src/components/instance-tabs-provider.tsx deleted file mode 100644 index 8de41cc0efc..00000000000 --- a/packages/compass-instance/src/components/instance-tabs-provider.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useContext, useMemo, useRef } from 'react'; - -export type InstanceTab = { - name: string; - component: React.ComponentType; -}; - -const InstanceTabsContext = React.createContext([]); - -export const InstanceTabsProvider: React.FunctionComponent<{ - tabs: InstanceTab[]; -}> = ({ tabs, children }) => { - const tabsRef = useRef(tabs); - return ( - - {children} - - ); -}; - -export function useInstanceTabs( - activeTabName: string | null, - filterFn: (tab: InstanceTab) => boolean = () => true -): [InstanceTab[], number] { - const tabs = useContext(InstanceTabsContext); - const filteredTabs = useMemo(() => { - return tabs.filter(filterFn); - }, [tabs, filterFn]); - const activeTabId = useMemo(() => { - return filteredTabs.findIndex((tab) => { - return tab.name === activeTabName; - }); - }, [filteredTabs, activeTabName]); - return [filteredTabs, activeTabId === -1 ? 0 : activeTabId]; -} diff --git a/packages/compass-instance/src/components/instance.spec.tsx b/packages/compass-instance/src/components/instance.spec.tsx deleted file mode 100644 index f6162852edc..00000000000 --- a/packages/compass-instance/src/components/instance.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { expect } from 'chai'; - -import { InstanceComponent } from './instance'; -import { InstanceTabsProvider } from './instance-tabs-provider'; - -const testText = 'Testing'; - -class Databases extends React.Component { - render() { - return
{testText}
; - } -} - -const ROLE = { - name: 'Databases', - component: Databases, -}; - -describe('Database [Component]', function () { - describe('when status is ready', function () { - beforeEach(function () { - render( - - { - /* noop */ - }} - activeTabName={ROLE.name} - /> - - ); - }); - - it('renders the tabs', function () { - expect(screen.getByText(testText)).to.be.visible; - }); - }); - - describe('when status is error', function () { - beforeEach(function () { - render( - { - /* noop */ - }} - activeTabName={null} - /> - ); - }); - - it('renders the error message', function () { - expect( - screen.getByText( - 'An error occurred while loading instance info: Pineapple' - ) - ).to.be.visible; - }); - - it('does not renders the tabs', function () { - expect(screen.queryByText(testText)).to.not.exist; - }); - }); -}); diff --git a/packages/compass-instance/src/components/instance.tsx b/packages/compass-instance/src/components/instance.tsx deleted file mode 100644 index 211dfd60e9b..00000000000 --- a/packages/compass-instance/src/components/instance.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; -import { - Banner, - BannerVariant, - ErrorBoundary, - TabNavBar, - css, - spacing, -} from '@mongodb-js/compass-components'; -import { useLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; -import type { InstanceTab } from './instance-tabs-provider'; -import { useInstanceTabs } from './instance-tabs-provider'; -import type { MongoDBInstance } from 'mongodb-instance-model'; - -function trackingIdForTabName({ name }: { name: string }) { - return name.toLowerCase().replace(/ /g, '_'); -} - -const errorContainerStyles = css({ - padding: spacing[3], -}); - -const ERROR_WARNING = 'An error occurred while loading instance info'; - -const NOT_MASTER_ERROR = 'not master and slaveOk=false'; - -// We recommend in the connection dialog to switch to these read preferences. -const RECOMMEND_READ_PREF_MSG = `It is recommended to change your read - preference in the connection dialog to Primary Preferred or Secondary Preferred - or provide a replica set name for a full topology connection.`; - -const instanceComponentContainerSyles = css({ - display: 'flex', - flexDirection: 'column', - alignItems: 'stretch', - height: '100%', -}); - -type InstanceComponentProps = { - activeTabName: string | null; - onTabClick: (name: string) => void; - isDataLake: boolean; - instanceInfoLoadingStatus: MongoDBInstance['status']; - instanceInfoLoadingError: string | null; -}; - -const InstanceComponent: React.FunctionComponent = ({ - activeTabName, - onTabClick, - isDataLake, - instanceInfoLoadingStatus, - instanceInfoLoadingError, -}) => { - const { track, log, mongoLogId } = useLoggerAndTelemetry('COMPASS-INSTANCE'); - const filterTabByName = useCallback( - (tab: InstanceTab) => { - switch (tab.name) { - case 'Performance': - return !isDataLake; - default: - return true; - } - }, - [isDataLake] - ); - const [tabs, activeTabIndex] = useInstanceTabs( - activeTabName, - filterTabByName - ); - - const activeTab = tabs[activeTabIndex]; - - useEffect(() => { - if (activeTab) { - track('Screen', { name: trackingIdForTabName(activeTab) }); - } - }, [activeTab, track]); - - if (instanceInfoLoadingStatus === 'error') { - if (instanceInfoLoadingError?.includes(NOT_MASTER_ERROR)) { - instanceInfoLoadingError = `'${instanceInfoLoadingError}': ${RECOMMEND_READ_PREF_MSG}`; - } - - return ( -
- - {ERROR_WARNING}: {instanceInfoLoadingError} - -
- ); - } - - if ( - instanceInfoLoadingStatus === 'ready' || - instanceInfoLoadingStatus === 'refreshing' - ) { - return ( -
- { - return tab.name; - })} - views={tabs.map((tab) => { - return ( - { - log.error( - mongoLogId(1_001_000_110), - 'Instance Workspace', - 'Rendering instance tab failed', - { name: tab.name, error: err.message, errorInfo } - ); - }} - > - - - ); - })} - activeTabIndex={activeTabIndex} - onTabClicked={(idx) => { - onTabClick(tabs[idx].name); - }} - /> -
- ); - } - - return null; -}; - -export { InstanceComponent }; diff --git a/packages/compass-instance/src/index.ts b/packages/compass-instance/src/index.ts deleted file mode 100644 index 6512ef12364..00000000000 --- a/packages/compass-instance/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { registerHadronPlugin } from 'hadron-app-registry'; -import InstanceWorkspace from './plugin'; -import { activatePlugin } from './stores'; -import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider'; - -function activate() { - // noop -} - -function deactivate() { - // noop -} - -const InstanceWorkspacePlugin = registerHadronPlugin( - { - name: 'InstanceWorkspace', - component: InstanceWorkspace, - activate: activatePlugin, - }, - { - instance: mongoDBInstanceLocator, - } -); - -export default InstanceWorkspacePlugin; -export type { InstanceTab } from './components/instance-tabs-provider'; -export { InstanceTabsProvider } from './components/instance-tabs-provider'; -export { activate, deactivate }; -export { default as metadata } from '../package.json'; diff --git a/packages/compass-instance/src/plugin.tsx b/packages/compass-instance/src/plugin.tsx deleted file mode 100644 index 1f6b60c78d7..00000000000 --- a/packages/compass-instance/src/plugin.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { connect } from 'react-redux'; -import type { State } from './stores'; -import { emitChangeTab } from './stores'; -import { InstanceComponent } from './components/instance'; - -const ConnectedInstanceComponent = connect((state: State) => state, { - onTabClick: emitChangeTab, -})(InstanceComponent); - -export default ConnectedInstanceComponent; diff --git a/packages/compass-instance/src/stores/index.ts b/packages/compass-instance/src/stores/index.ts deleted file mode 100644 index b2f6c746bbe..00000000000 --- a/packages/compass-instance/src/stores/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { ThunkAction } from 'redux-thunk'; -import thunk from 'redux-thunk'; -import { createStore, applyMiddleware } from 'redux'; -import type AppRegistry from 'hadron-app-registry'; -import type { AnyAction, Reducer, Action } from 'redux'; -import type { MongoDBInstance } from 'mongodb-instance-model'; - -export type State = { - activeTabName: string | null; - instanceInfoLoadingStatus: MongoDBInstance['status']; - instanceInfoLoadingError: string | null; - isDataLake: boolean; -}; - -const INITIAL_STATE = { - activeTabName: null, - instanceInfoLoadingStatus: 'initial', - instanceInfoLoadingError: null, - isDataLake: false, -}; - -type InstanceWorkspaceThunkAction< - R, - A extends Action = AnyAction -> = ThunkAction< - R, - State, - Pick, - A ->; - -type InstanceWorkspaceServices = { - globalAppRegistry: AppRegistry; - instance: MongoDBInstance; -}; - -const CHANGE_TAB = 'change-tab'; - -export const changeTab = (tabName: string) => { - return { type: CHANGE_TAB, tabName }; -}; - -export const emitChangeTab = ( - tabName: string -): InstanceWorkspaceThunkAction => { - return (_dispatch, _getState, { globalAppRegistry }) => { - // By emitting open-instance-workspace rather than change-tab directly, - // the clicks on the tabs work the same way compared to when we select a - // tab from the outside. That way things like the sidebar can be aware - // that the instance tab is changing. - // - // TODO(COMPASS-7354): Will go away with workspaces plugin - globalAppRegistry.emit('open-instance-workspace', tabName); - }; -}; - -const MONGODB_INSTANCE_INFO_STATUS_CHANGED = - 'mongodb-instance-info-status-changed'; - -const instanceInfoStatusChanged = (instance: MongoDBInstance) => { - return { - type: MONGODB_INSTANCE_INFO_STATUS_CHANGED, - status: instance.status, - error: instance.statusError, - isDataLake: instance.dataLake.isDataLake, - }; -}; - -const reducer: Reducer = (state = { ...INITIAL_STATE }, action) => { - if (action.type === CHANGE_TAB) { - return { - ...state, - activeTabName: action.tabName, - }; - } - - if (action.type === MONGODB_INSTANCE_INFO_STATUS_CHANGED) { - return { - ...state, - instanceInfoLoadingStatus: action.status, - instanceInfoLoadingError: action.error, - isDataLake: action.isDataLake, - }; - } - - return state; -}; - -export function activatePlugin( - _: unknown, - { globalAppRegistry, instance }: InstanceWorkspaceServices -) { - const store = createStore( - reducer, - { - ...INITIAL_STATE, - isDataLake: instance.dataLake.isDataLake, - instanceInfoLoadingStatus: instance.status, - instanceInfoLoadingError: instance.statusError, - }, - applyMiddleware(thunk.withExtraArgument({ globalAppRegistry })) - ); - - const onOpenInstanceWorkspace = (tabName: string) => { - store.dispatch(changeTab(tabName)); - }; - - globalAppRegistry.on('open-instance-workspace', onOpenInstanceWorkspace); - - const onInstanceStatusChanged = () => { - store.dispatch(instanceInfoStatusChanged(instance)); - }; - - instance.on('change:status', onInstanceStatusChanged); - - return { - store, - deactivate() { - globalAppRegistry.removeListener( - 'open-instance-workspace', - onOpenInstanceWorkspace - ); - instance.removeListener('change:status', onInstanceStatusChanged); - }, - }; -} diff --git a/packages/compass-instance/src/typings.d.ts b/packages/compass-instance/src/typings.d.ts deleted file mode 100644 index 43b4ac609bf..00000000000 --- a/packages/compass-instance/src/typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@mongodb-js/mongodb-redux-common/app-registry'; diff --git a/packages/compass-instance/tsconfig-lint.json b/packages/compass-instance/tsconfig-lint.json deleted file mode 100644 index 6bdef84f322..00000000000 --- a/packages/compass-instance/tsconfig-lint.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/compass-instance/webpack.config.js b/packages/compass-instance/webpack.config.js deleted file mode 100644 index ae979cb7d59..00000000000 --- a/packages/compass-instance/webpack.config.js +++ /dev/null @@ -1,2 +0,0 @@ -const { compassPluginConfig } = require('@mongodb-js/webpack-config-compass'); -module.exports = compassPluginConfig; diff --git a/packages/compass-query-bar/package.json b/packages/compass-query-bar/package.json index cd0c125398a..3b992a9ac93 100644 --- a/packages/compass-query-bar/package.json +++ b/packages/compass-query-bar/package.json @@ -78,7 +78,7 @@ "@testing-library/user-event": "^13.5.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.14", "lodash": "^4.17.21", diff --git a/packages/compass-saved-aggregations-queries/src/components/aggregations-queries-list.tsx b/packages/compass-saved-aggregations-queries/src/components/aggregations-queries-list.tsx index d3ca779cf84..7c47eb97284 100644 --- a/packages/compass-saved-aggregations-queries/src/components/aggregations-queries-list.tsx +++ b/packages/compass-saved-aggregations-queries/src/components/aggregations-queries-list.tsx @@ -105,6 +105,14 @@ export const AggregationsQueriesList = ({ }) .map((x) => x.item); + useTrackOnChange( + 'COMPASS-MY-QUERIES-UI', + (track) => { + track('Screen', { name: 'my_queries' }); + }, + [] + ); + useTrackOnChange( 'COMPASS-MY-QUERIES-UI', (track) => { @@ -240,4 +248,7 @@ const mapDispatch = { onCopyToClipboard: copyToClipboard, }; -export default connect(mapState, mapDispatch)(AggregationsQueriesList); +export default connect( + mapState, + mapDispatch +)(AggregationsQueriesList) as React.FunctionComponent>; diff --git a/packages/compass-saved-aggregations-queries/src/index.ts b/packages/compass-saved-aggregations-queries/src/index.ts index 96e43ce5a39..ea802bb006c 100644 --- a/packages/compass-saved-aggregations-queries/src/index.ts +++ b/packages/compass-saved-aggregations-queries/src/index.ts @@ -46,11 +46,15 @@ export const MyQueriesPlugin = registerHadronPlugin< serviceLocators ); -const InstanceTab = { - name: 'My Queries', +export const WorkspaceTab = { + name: 'My Queries' as const, component: MyQueriesPlugin, }; -export default InstanceTab; +export type MyQueriesWorkspace = { + type: typeof WorkspaceTab['name']; +} & React.ComponentProps; + +export default MyQueriesPlugin; export { activate, deactivate }; export { default as metadata } from '../package.json'; diff --git a/packages/compass-saved-aggregations-queries/src/stores/index.ts b/packages/compass-saved-aggregations-queries/src/stores/index.ts index 5013762b6d0..2a865e14651 100644 --- a/packages/compass-saved-aggregations-queries/src/stores/index.ts +++ b/packages/compass-saved-aggregations-queries/src/stores/index.ts @@ -65,7 +65,7 @@ export type SavedQueryAggregationThunkAction< > = ThunkAction; export function activatePlugin( - _: unknown, + _: Record, services: MyQueriesServices & Partial ) { const store = configureStore(services); diff --git a/packages/compass-schema-validation/package.json b/packages/compass-schema-validation/package.json index 0a9ab56c7a6..c7a816bee34 100644 --- a/packages/compass-schema-validation/package.json +++ b/packages/compass-schema-validation/package.json @@ -77,7 +77,7 @@ "@mongodb-js/webpack-config-compass": "^1.2.5", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-ipc": "^3.2.4", diff --git a/packages/compass-serverstats/.depcheckrc b/packages/compass-serverstats/.depcheckrc index 8df91b5de8d..7cf4acd054f 100644 --- a/packages/compass-serverstats/.depcheckrc +++ b/packages/compass-serverstats/.depcheckrc @@ -4,4 +4,6 @@ ignores: - "mongodb-compass" # webpack always externalizes 'clipboard' for legacy reasons - "clipboard" - - "react-dom" \ No newline at end of file + - "react-dom" +ignore-patterns: + - 'dist' diff --git a/packages/compass-serverstats/src/components/index.tsx b/packages/compass-serverstats/src/components/index.tsx index 07217eeae8c..ee4fffc00f5 100644 --- a/packages/compass-serverstats/src/components/index.tsx +++ b/packages/compass-serverstats/src/components/index.tsx @@ -21,6 +21,7 @@ import TopStore from '../stores/top-store'; import { ServerStatsToolbar } from './server-stats-toolbar'; import Actions from '../actions'; import type { TimeScrubEventDispatcher } from './server-stats-toolbar'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const REFRESH_STATS_INTERVAL_MS = 1000; @@ -115,6 +116,14 @@ function PerformancePanelMsgs() { function PerformanceComponent() { const eventDispatcher = useRef(realTimeDispatcher()); + useTrackOnChange( + 'COMPASS-PERFORMANCE-UI', + (track) => { + track('Screen', { name: 'performance' }); + }, + [] + ); + useEffect(() => { return () => { // Reset the store on unmount so that when this is remounted diff --git a/packages/compass-serverstats/src/index.ts b/packages/compass-serverstats/src/index.ts index e0a6d663a3f..84e5efbfcbc 100644 --- a/packages/compass-serverstats/src/index.ts +++ b/packages/compass-serverstats/src/index.ts @@ -14,7 +14,7 @@ const PerformancePlugin = registerHadronPlugin( { name: 'Performance', component: PerformanceComponent, - activate(_: unknown, { dataService, instance }) { + activate(_initialProps: Record, { dataService, instance }) { CurrentOpStore.onActivated(dataService); ServerStatsStore.onActivated(dataService); TopStore.onActivated(dataService, instance); @@ -35,11 +35,15 @@ const PerformancePlugin = registerHadronPlugin( } ); -const InstanceTab = { - name: 'Performance', +const WorkspaceTab = { + name: 'Performance' as const, component: PerformancePlugin, }; +export type ServerStatsWorkspace = { + type: typeof WorkspaceTab['name']; +} & React.ComponentProps; + /** * Activate all the components in the RTSS package. */ @@ -55,6 +59,6 @@ function deactivate() { } export default PerformancePlugin; -export { activate, deactivate, InstanceTab }; +export { activate, deactivate, WorkspaceTab }; export { default as d3 } from './d3'; export { default as metadata } from '../package.json'; diff --git a/packages/compass-shell/package.json b/packages/compass-shell/package.json index 65ebf2a4875..49a8647700b 100644 --- a/packages/compass-shell/package.json +++ b/packages/compass-shell/package.json @@ -87,7 +87,7 @@ "@mongosh/logging": "^2.1.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "enzyme": "^3.11.0", "eslint": "^7.25.0", "mocha": "^10.2.0", diff --git a/packages/compass-sidebar/src/components/database-collection-filter.tsx b/packages/compass-sidebar/src/components/database-collection-filter.tsx index 2657212f182..158d798e96a 100644 --- a/packages/compass-sidebar/src/components/database-collection-filter.tsx +++ b/packages/compass-sidebar/src/components/database-collection-filter.tsx @@ -6,9 +6,9 @@ const databaseCollectionsFilter = css({ }); export default function DatabaseCollectionFilter({ - changeFilterRegex, + onFilterChange, }: { - changeFilterRegex(regex: RegExp | null): void; + onFilterChange(regex: RegExp | null): void; }) { const onChange = useCallback( (event) => { @@ -22,9 +22,9 @@ export default function DatabaseCollectionFilter({ re = null; } - changeFilterRegex(re); + onFilterChange(re); }, - [changeFilterRegex] + [onFilterChange] ); const onSubmit = useCallback((evt) => { diff --git a/packages/compass-sidebar/src/components/navigation-items.spec.tsx b/packages/compass-sidebar/src/components/navigation-items.spec.tsx index 4c7874f4cfe..713c959efcf 100644 --- a/packages/compass-sidebar/src/components/navigation-items.spec.tsx +++ b/packages/compass-sidebar/src/components/navigation-items.spec.tsx @@ -19,9 +19,9 @@ function renderNavigationItems( onAction={() => { /* noop */ }} - isDataLake={false} - isWritable={true} - changeFilterRegex={() => { + showPerformanceItem={false} + showCreateDatabaseAction={true} + onFilterChange={() => { /* noop */ }} currentLocation={null} @@ -52,7 +52,7 @@ describe('NavigationItems [Component]', function () { describe('when rendered read only', function () { it('does not render the create database button', function () { renderNavigationItems({ - readOnly: true, + showCreateDatabaseAction: false, }); expect(screen.queryByLabelText(createDatabaseText)).to.not.exist; }); diff --git a/packages/compass-sidebar/src/components/navigation-items.tsx b/packages/compass-sidebar/src/components/navigation-items.tsx index 5c0978aeba7..d0b53aaf8d1 100644 --- a/packages/compass-sidebar/src/components/navigation-items.tsx +++ b/packages/compass-sidebar/src/components/navigation-items.tsx @@ -186,24 +186,21 @@ export function NavigationItem({ export function NavigationItems({ isExpanded, - isDataLake, - isWritable, - changeFilterRegex, + showCreateDatabaseAction = false, + showPerformanceItem = false, + onFilterChange, onAction, currentLocation, - readOnly, showTooManyCollectionsInsight = false, }: { isExpanded?: boolean; - isDataLake?: boolean; - isWritable?: boolean; - changeFilterRegex(regex: RegExp | null): void; + showCreateDatabaseAction?: boolean; + showPerformanceItem?: boolean; + onFilterChange(regex: RegExp | null): void; onAction(actionName: string, ...rest: any[]): void; currentLocation: string | null; - readOnly?: boolean; showTooManyCollectionsInsight?: boolean; }) { - const isReadOnly = readOnly || isDataLake || !isWritable; const databasesActions = useMemo(() => { const actions: ItemAction[] = [ { @@ -213,7 +210,7 @@ export function NavigationItems({ }, ]; - if (!isReadOnly) { + if (showCreateDatabaseAction) { actions.push({ action: 'open-create-database', label: 'Create database', @@ -222,7 +219,7 @@ export function NavigationItems({ } return actions; - }, [isReadOnly]); + }, [showCreateDatabaseAction]); return ( <> @@ -234,6 +231,16 @@ export function NavigationItems({ tabName="My Queries" isActive={currentLocation === 'My Queries'} /> + {showPerformanceItem && ( + + isExpanded={isExpanded} + onAction={onAction} + glyph="Gauge" + label="Performance" + tabName="Performance" + isActive={currentLocation === 'Performance'} + /> + )} isExpanded={isExpanded} onAction={onAction} @@ -245,14 +252,37 @@ export function NavigationItems({ showTooManyCollectionsInsight={showTooManyCollectionsInsight} /> {isExpanded && ( - + )} {isExpanded && } ); } -const mapStateToProps = (state: RootState) => { +/** + * Returns either current instance value for a key or a specified default in + * case we haven't fetched instance info yet + */ +function getInstanceValue( + state: RootState, + key: 'isDataLake' | 'isWritable', + defaultValue = false +) { + const instanceFetched = ['refreshing', 'ready'].includes( + state.instance?.status ?? '' + ); + if (key === 'isDataLake') { + return instanceFetched ? state.instance?.dataLake.isDataLake : defaultValue; + } + if (key === 'isWritable') { + return instanceFetched ? state.instance?.isWritable : defaultValue; + } +} + +const mapStateToProps = ( + state: RootState, + { readOnly: preferencesReadOnly }: { readOnly: boolean } +) => { const totalCollectionsCount = state.databases.databases.reduce( (acc: number, db: { collectionsLength: number }) => { return acc + db.collectionsLength; @@ -262,14 +292,26 @@ const mapStateToProps = (state: RootState) => { return { currentLocation: state.location, - isDataLake: state.instance?.dataLake.isDataLake, - isWritable: state.instance?.isWritable, + showPerformanceItem: + // For default `isDataLake` value we're choosing the one that will hide + // the items that would otherwise not work for the ADF + !getInstanceValue(state, 'isDataLake', true), + showCreateDatabaseAction: + !getInstanceValue(state, 'isDataLake', true) && + // ... same with `isWritable`, a safe default here is the one that allows + // to do less while we're getting the info + getInstanceValue(state, 'isWritable', false) && + !preferencesReadOnly, showTooManyCollectionsInsight: totalCollectionsCount > 10_000, }; }; -const MappedNavigationItems = connect(mapStateToProps, { - changeFilterRegex, -})(withPreferences(NavigationItems, ['readOnly'], React)); +const MappedNavigationItems = withPreferences( + connect(mapStateToProps, { + onFilterChange: changeFilterRegex, + })(NavigationItems), + ['readOnly'], + React +); export default MappedNavigationItems; diff --git a/packages/compass-sidebar/src/components/sidebar.tsx b/packages/compass-sidebar/src/components/sidebar.tsx index c4e374109df..27d50d70964 100644 --- a/packages/compass-sidebar/src/components/sidebar.tsx +++ b/packages/compass-sidebar/src/components/sidebar.tsx @@ -148,6 +148,7 @@ export function Sidebar({ collapsable={true} expanded={isExpanded} setExpanded={setIsExpanded} + data-testid="navigation-sidebar" > <> { store.dispatch( changeInstance({ + status: instance.status, refreshingStatus: instance.refreshingStatus, databasesStatus: instance.databasesStatus, csfleMode: instance.csfleMode, @@ -61,9 +62,17 @@ export function createSidebarStore({ ); }; - const onInstanceChange = throttle((instance) => { - onInstanceChangeNow(instance); - }, 300); + const onInstanceChange = throttle( + (instance) => { + onInstanceChangeNow(instance); + }, + 300, + { leading: true, trailing: true } + ); + + cleanup.push(() => { + onInstanceChange.cancel(); + }); function getDatabaseInfo(db: Database) { return { @@ -82,18 +91,26 @@ export function createSidebarStore({ }; } - const onDatabasesChange = throttle((databases: Database[]) => { - const dbs = databases.map((db) => { - return { - ...getDatabaseInfo(db), - collections: db.collections.map((coll) => { - return getCollectionInfo(coll); - }), - }; - }); + const onDatabasesChange = throttle( + (databases: Database[]) => { + const dbs = databases.map((db) => { + return { + ...getDatabaseInfo(db), + collections: db.collections.map((coll) => { + return getCollectionInfo(coll); + }), + }; + }); + + store.dispatch(changeDatabases(dbs)); + }, + 300, + { leading: true, trailing: true } + ); - store.dispatch(changeDatabases(dbs)); - }, 300); + cleanup.push(() => { + onDatabasesChange.cancel(); + }); store.dispatch(globalAppRegistryActivated(globalAppRegistry)); @@ -125,13 +142,12 @@ export function createSidebarStore({ onInstanceChange(instance); }); + onInstanceEvent('change:status', () => { + onInstanceChange(instance); + }); + onInstanceEvent('change:refreshingStatus', () => { - // This will always fire when we start fetching the instance details which - // will cause a 300ms throttle before any instance details can update if - // we send it though the throttled update. That's long enough for the - // sidebar to display that we're connected to a standalone instance when - // we're really connected to dataLake. - onInstanceChangeNow(instance); + onInstanceChange(instance); }); onInstanceEvent('change:databasesStatus', () => { @@ -147,31 +163,13 @@ export function createSidebarStore({ onDatabasesChange(instance.databases); }); - on(instance.build as any, 'change:isEnterprise', () => { - onInstanceChange(instance); - }); - - on(instance.build as any, 'change:version', () => { - onInstanceChange(instance); - }); - - on(instance.dataLake as any, 'change:isDataLake', () => { - onInstanceChange(instance); - }); - - on(instance.dataLake as any, 'change:version', () => { - onInstanceChange(instance); - }); - store.dispatch( toggleIsGenuineMongoDBVisible(!instance.genuineMongoDB.isGenuine) ); - on( - instance.genuineMongoDB as any, - 'change:isGenuine', - (model: unknown, isGenuine: boolean) => { - onInstanceChange(instance); // isGenuineMongoDB is part of instance state + onInstanceEvent( + 'change:genuineMongoDB.isGenuine', + (_model: unknown, isGenuine: boolean) => { store.dispatch(toggleIsGenuineMongoDBVisible(!isGenuine)); } ); diff --git a/packages/compass-utils/package.json b/packages/compass-utils/package.json index 1fdb1770792..35469c2febe 100644 --- a/packages/compass-utils/package.json +++ b/packages/compass-utils/package.json @@ -51,7 +51,7 @@ }, "optionalDependencies": { "@electron/remote": "^2.1.0", - "electron": "^25.9.6" + "electron": "^25.9.7" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.11", diff --git a/packages/compass-workspaces/.depcheckrc b/packages/compass-workspaces/.depcheckrc new file mode 100644 index 00000000000..ae7c8273e41 --- /dev/null +++ b/packages/compass-workspaces/.depcheckrc @@ -0,0 +1,11 @@ +ignores: + - '@mongodb-js/prettier-config-compass' + - '@mongodb-js/tsconfig-compass' + - '@types/chai' + - '@types/sinon-chai' + - 'sinon' + - '@types/chai-dom' + - '@types/react' + - '@types/react-dom' +ignore-patterns: + - 'dist' diff --git a/packages/compass-database/.eslintignore b/packages/compass-workspaces/.eslintignore similarity index 100% rename from packages/compass-database/.eslintignore rename to packages/compass-workspaces/.eslintignore diff --git a/packages/compass-database/.eslintrc.js b/packages/compass-workspaces/.eslintrc.js similarity index 78% rename from packages/compass-database/.eslintrc.js rename to packages/compass-workspaces/.eslintrc.js index f4285e89285..e4cf824b6ac 100644 --- a/packages/compass-database/.eslintrc.js +++ b/packages/compass-workspaces/.eslintrc.js @@ -5,8 +5,4 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig-lint.json'], }, - env: { - node: true, - browser: true, - }, }; diff --git a/packages/compass-database/.mocharc.js b/packages/compass-workspaces/.mocharc.js similarity index 100% rename from packages/compass-database/.mocharc.js rename to packages/compass-workspaces/.mocharc.js diff --git a/packages/compass-database/.prettierignore b/packages/compass-workspaces/.prettierignore similarity index 65% rename from packages/compass-database/.prettierignore rename to packages/compass-workspaces/.prettierignore index ec0e36b246c..4d28df6603a 100644 --- a/packages/compass-database/.prettierignore +++ b/packages/compass-workspaces/.prettierignore @@ -1,3 +1,3 @@ .nyc_output dist -coverage \ No newline at end of file +coverage diff --git a/packages/compass-database/.prettierrc.json b/packages/compass-workspaces/.prettierrc.json similarity index 100% rename from packages/compass-database/.prettierrc.json rename to packages/compass-workspaces/.prettierrc.json diff --git a/packages/compass-instance/package.json b/packages/compass-workspaces/package.json similarity index 73% rename from packages/compass-instance/package.json rename to packages/compass-workspaces/package.json index e10dae46dde..61c645cb932 100644 --- a/packages/compass-instance/package.json +++ b/packages/compass-workspaces/package.json @@ -1,8 +1,7 @@ { - "name": "@mongodb-js/compass-instance", - "productName": "Instance plugin", - "description": "Compass instance plugin", - "version": "4.19.1", + "name": "@mongodb-js/compass-workspaces", + "productName": "compass-workspaces Plugin", + "description": "Compass plugin responsible for rendering and managing state of current namespace / workspace", "author": { "name": "MongoDB Inc", "email": "compass@mongodb.com" @@ -10,22 +9,22 @@ "publishConfig": { "access": "public" }, - "repository": { - "type": "git", - "url": "https://github.com/mongodb-js/compass.git" - }, - "homepage": "https://github.com/mongodb-js/compass", "bugs": { "url": "https://jira.mongodb.org/projects/COMPASS/issues", "email": "compass@mongodb.com" }, - "license": "SSPL", + "homepage": "https://github.com/mongodb-js/compass", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/compass.git" + }, "files": [ "dist" ], + "license": "SSPL", "main": "dist/index.js", "compass:main": "src/index.ts", - "types": "dist/src/index.d.ts", "exports": { "browser": "./dist/browser.js", "require": "./dist/index.js" @@ -33,12 +32,14 @@ "compass:exports": { ".": "./src/index.ts" }, + "types": "./dist/src/index.d.ts", "scripts": { "bootstrap": "npm run postcompile", "prepublishOnly": "npm run compile && compass-scripts check-exports-exist", "compile": "npm run webpack -- --mode production", "webpack": "webpack-compass", "postcompile": "tsc --emitDeclarationOnly", + "start": "npm run webpack serve -- --mode development", "analyze": "npm run webpack -- --mode production --analyze", "typecheck": "tsc -p tsconfig-lint.json --noEmit", "eslint": "eslint", @@ -59,36 +60,50 @@ "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-logging": "^1.2.6", + "bson": "^6.2.0", "hadron-app-registry": "^9.0.14", "react": "^17.0.2" }, + "dependencies": { + "@mongodb-js/compass-app-stores": "^7.6.1", + "@mongodb-js/compass-components": "^1.19.0", + "@mongodb-js/compass-logging": "^1.2.6", + "bson": "^6.2.0", + "hadron-app-registry": "^9.0.14" + }, "devDependencies": { + "@mongodb-js/compass-collection": "^4.19.1", + "@mongodb-js/compass-databases-collections": "^1.19.1", + "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", + "@mongodb-js/compass-serverstats": "^16.19.1", "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/mocha-config-compass": "^1.3.2", "@mongodb-js/prettier-config-compass": "^1.0.1", "@mongodb-js/tsconfig-compass": "^1.0.3", "@mongodb-js/webpack-config-compass": "^1.2.5", "@testing-library/react": "^12.1.4", + "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", + "@types/chai-dom": "^0.0.10", "@types/mocha": "^9.0.0", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", - "chai": "^4.3.4", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.3.6", "depcheck": "^1.4.1", "eslint": "^7.25.0", + "lodash": "^4.17.21", "mocha": "^10.2.0", - "mongodb-instance-model": "^12.15.1", + "mongodb-collection-model": "^5.15.1", + "mongodb-database-model": "^2.15.1", + "mongodb-ns": "^2.4.0", "nyc": "^15.1.0", - "react": "^17.0.2", + "prettier": "^2.7.1", "react-dom": "^17.0.2", - "react-redux": "^8.0.5", + "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.1" - }, - "dependencies": { - "@mongodb-js/compass-app-stores": "^7.6.1", - "@mongodb-js/compass-components": "^1.19.0", - "@mongodb-js/compass-logging": "^1.2.6", - "hadron-app-registry": "^9.0.14" + "redux-thunk": "^2.4.2", + "sinon": "^17.0.1", + "xvfb-maybe": "^0.2.1" } } diff --git a/packages/compass-workspaces/src/components/workspaces-provider.tsx b/packages/compass-workspaces/src/components/workspaces-provider.tsx new file mode 100644 index 00000000000..791f896150c --- /dev/null +++ b/packages/compass-workspaces/src/components/workspaces-provider.tsx @@ -0,0 +1,45 @@ +import React, { useContext, useRef, useCallback } from 'react'; +import type { AnyWorkspace } from '../stores/workspaces'; + +export type WorkspaceComponent = { + name: T; + component: React.FunctionComponent< + Omit, 'type'> + >; +}; + +export type AnyWorkspaceComponent = + | WorkspaceComponent<'My Queries'> + | WorkspaceComponent<'Performance'> + | WorkspaceComponent<'Databases'> + | WorkspaceComponent<'Collections'> + | WorkspaceComponent<'Collection'>; + +const WorkspacesContext = React.createContext([]); + +export const WorkspacesProvider: React.FunctionComponent<{ + value: AnyWorkspaceComponent[]; +}> = ({ value, children }) => { + const valueRef = useRef(value); + return ( + + {children} + + ); +}; + +export const useWorkspacePlugin = () => { + const workspaces = useContext(WorkspacesContext); + return useCallback( + (name: T) => { + const plugin = workspaces.find((workspace) => workspace.name === name); + if (!plugin) { + throw new Error( + `Component for workspace "${name}" is missing in context. Did you forget to set up WorkspacesProvider?` + ); + } + return plugin.component as unknown as WorkspaceComponent['component']; + }, + [workspaces] + ); +}; diff --git a/packages/compass-workspaces/src/components/workspaces.tsx b/packages/compass-workspaces/src/components/workspaces.tsx new file mode 100644 index 00000000000..136a7aba0d7 --- /dev/null +++ b/packages/compass-workspaces/src/components/workspaces.tsx @@ -0,0 +1,239 @@ +import React, { useMemo, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { AppRegistryProvider } from 'hadron-app-registry'; +import { + ErrorBoundary, + MongoDBLogoMark, + WorkspaceTabs, + css, + spacing, + useDarkMode, +} from '@mongodb-js/compass-components'; +import type { + OpenWorkspaceOptions, + WorkspaceTab, + WorkspacesState, +} from '../stores/workspaces'; +import { + closeTab, + getActiveTab, + getLocalAppRegistryForTab, + moveTab, + emitOnTabChange, + openTabFromCurrent, + selectNextTab, + selectPrevTab, + selectTab, +} from '../stores/workspaces'; +import { useWorkspacePlugin } from './workspaces-provider'; +import toNS from 'mongodb-ns'; +import { useLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; + +const emptyWorkspaceStyles = css({ + margin: '0 auto', + alignSelf: 'center', + opacity: 0.05, +}); + +const EmptyWorkspaceContent = () => { + const darkMode = useDarkMode(); + return ( +
+ +
+ ); +}; + +const workspacesContainerStyles = css({ + display: 'grid', + width: '100%', + height: '100%', + gridTemplateColumns: '100%', + gridTemplateRows: 'auto 1fr', +}); + +const workspacesContentStyles = css({ + display: 'flex', + flex: 1, + minHeight: 0, +}); + +type CompassWorkspacesProps = { + tabs: WorkspaceTab[]; + activeTab?: WorkspaceTab | null; + initialTab?: OpenWorkspaceOptions; + + onSelectTab(at: number): void; + onSelectNextTab(): void; + onSelectPrevTab(): void; + onMoveTab(from: number, to: number): void; + onCreateTab(): void; + onCloseTab(at: number): void; + + onTabChange?: (tab: WorkspaceTab) => void; +}; + +const CompassWorkspaces: React.FunctionComponent = ({ + tabs, + activeTab, + onSelectTab, + onSelectNextTab, + onSelectPrevTab, + onMoveTab, + onCreateTab, + onCloseTab, + onTabChange, +}) => { + const { log, mongoLogId } = useLoggerAndTelemetry('COMPASS-WORKSPACES'); + const getWorkspaceByName = useWorkspacePlugin(); + + useEffect(() => { + if (activeTab) { + onTabChange?.(activeTab); + } + }, [ + onTabChange, + activeTab?.type, + (activeTab as { namespace: string } | undefined)?.namespace, + ]); + + const tabDescriptions = useMemo(() => { + return tabs.map((tab) => { + switch (tab.type) { + case 'My Queries': + return { + id: tab.id, + title: tab.type, + iconGlyph: 'CurlyBraces', + } as const; + case 'Databases': + return { + id: tab.id, + title: tab.type, + iconGlyph: 'Database', + } as const; + case 'Performance': + return { + id: tab.id, + title: tab.type, + iconGlyph: 'Gauge', + } as const; + case 'Collections': + return { + id: tab.id, + title: tab.namespace, + iconGlyph: 'Database', + } as const; + case 'Collection': { + const { database, collection } = toNS(tab.namespace); + // TODO: make sure metadata is resolved by collection-tab + const collectionType = tab.isTimeSeries + ? 'timeseries' + : tab.isReadonly + ? 'view' + : 'collection'; + return { + id: tab.id, + title: collection, + subtitle: `${database} > ${collection}`, + iconGlyph: + collectionType === 'view' + ? 'Visibility' + : collectionType === 'timeseries' + ? 'TimeSeries' + : 'Folder', + } as const; + } + } + }); + }, [tabs]); + + const activeTabIndex = tabs.findIndex((tab) => tab === activeTab); + + const activeWorkspaceElement = useMemo(() => { + switch (activeTab?.type) { + case 'My Queries': + case 'Performance': + case 'Databases': { + const Component = getWorkspaceByName(activeTab.type); + return ; + } + case 'Collections': { + const Component = getWorkspaceByName(activeTab.type); + return ; + } + case 'Collection': { + const Component = getWorkspaceByName(activeTab.type); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, type, ...collectionMetadata } = activeTab; + // TODO: make sure metadata is resolved by collection-tab + return ; + } + default: + return null; + } + }, [activeTab, getWorkspaceByName]); + + return ( +
+ + +
+ {activeTab && activeWorkspaceElement ? ( + + { + log.error( + mongoLogId(1_001_000_277), + 'Workspace', + 'Rendering workspace tab failed', + { name: activeTab.type, error: error.message, errorInfo } + ); + }} + > + {activeWorkspaceElement} + + + ) : ( + + )} +
+
+ ); +}; + +export default connect( + (state: WorkspacesState) => { + return { + tabs: state.tabs, + activeTab: getActiveTab(state), + }; + }, + { + onSelectTab: selectTab, + onSelectNextTab: selectNextTab, + onSelectPrevTab: selectPrevTab, + onMoveTab: moveTab, + onCreateTab: openTabFromCurrent, + onCloseTab: closeTab, + onTabChange: emitOnTabChange, + } +)(CompassWorkspaces); diff --git a/packages/compass-workspaces/src/index.spec.tsx b/packages/compass-workspaces/src/index.spec.tsx new file mode 100644 index 00000000000..f2006c49e57 --- /dev/null +++ b/packages/compass-workspaces/src/index.spec.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { expect } from 'chai'; +import { render, cleanup, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import WorkspacesPlugin, { WorkspacesProvider } from './index'; +import Sinon from 'sinon'; +import AppRegistry from 'hadron-app-registry'; +import type { AnyWorkspaceComponent } from './components/workspaces-provider'; + +function mockWorkspace(name: string) { + return { + name, + component: function Component() { + return <>{name}; + }, + } as unknown as AnyWorkspaceComponent; +} + +describe('WorkspacesPlugin', function () { + const sandbox = Sinon.createSandbox(); + const globalAppRegistry = sandbox.spy(new AppRegistry()); + const instance = { on() {}, removeListener() {} } as any; + const Plugin = WorkspacesPlugin.withMockServices({ + globalAppRegistry, + instance, + }); + + function renderPlugin() { + return render( + + + + ); + } + + afterEach(function () { + sandbox.resetHistory(); + cleanup(); + }); + + const tabs = [ + ['My Queries', ['open-instance-workspace']], + ['Databases', ['open-instance-workspace', 'Databases']], + ['Performance', ['open-instance-workspace', 'Performance']], + ['db', ['select-database', 'db']], + ['db > coll', ['select-namespace', { namespace: 'db.coll' }]], + ['db > coll', ['open-namespace-in-new-tab', { namespace: 'db.coll' }]], + ] as const; + + for (const suite of tabs) { + const [tabName, event] = suite; + it(`should open "${tabName}" tab on ${event[0]} event`, function () { + renderPlugin(); + globalAppRegistry.emit(event[0], event[1]); + expect(screen.getByRole('tab', { name: tabName })).to.exist; + }); + } + + it('should switch tabs when tab is clicked', async function () { + renderPlugin(); + globalAppRegistry.emit('open-namespace-in-new-tab', { + namespace: 'db.coll1', + }); + globalAppRegistry.emit('open-namespace-in-new-tab', { + namespace: 'db.coll2', + }); + globalAppRegistry.emit('open-namespace-in-new-tab', { + namespace: 'db.coll3', + }); + + expect(screen.getByRole('tab', { name: 'db > coll3' })).to.have.attribute( + 'aria-selected', + 'true' + ); + + userEvent.click(screen.getByRole('tab', { name: 'db > coll1' })); + + await waitFor(() => { + expect(screen.getByRole('tab', { name: 'db > coll3' })).to.have.attribute( + 'aria-selected', + 'false' + ); + expect(screen.getByRole('tab', { name: 'db > coll1' })).to.have.attribute( + 'aria-selected', + 'true' + ); + }); + }); +}); diff --git a/packages/compass-workspaces/src/index.ts b/packages/compass-workspaces/src/index.ts new file mode 100644 index 00000000000..426cdb34ece --- /dev/null +++ b/packages/compass-workspaces/src/index.ts @@ -0,0 +1,155 @@ +import type AppRegistry from 'hadron-app-registry'; +import type { ActivateHelpers } from 'hadron-app-registry'; +import { registerHadronPlugin } from 'hadron-app-registry'; +import type { OpenWorkspaceOptions } from './stores/workspaces'; +import workspacesReducer, { + collectionRemoved, + collectionRenamed, + databaseRemoved, + getActiveTab, + getInitialTabState, + getLocalAppRegistryForTab, + openWorkspace, +} from './stores/workspaces'; +import Workspaces from './components/workspaces'; +import { applyMiddleware, createStore } from 'redux'; +import thunk from 'redux-thunk'; +import type { CollectionTabPluginMetadata } from '@mongodb-js/compass-collection'; +import type { MongoDBInstance } from '@mongodb-js/compass-app-stores/provider'; +import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider'; +import type Collection from 'mongodb-collection-model'; +import type Database from 'mongodb-database-model'; + +export type WorkspacesServices = { + globalAppRegistry: AppRegistry; + instance: MongoDBInstance; +}; + +export function activateWorkspacePlugin( + { initialTab }: { initialTab?: OpenWorkspaceOptions }, + { globalAppRegistry, instance }: WorkspacesServices, + { on, cleanup }: ActivateHelpers +) { + const initialTabs = initialTab ? [getInitialTabState(initialTab)] : []; + + const store = createStore( + workspacesReducer, + { + tabs: initialTabs, + activeTabId: initialTabs[initialTabs.length - 1]?.id ?? null, + }, + applyMiddleware(thunk.withExtraArgument({ globalAppRegistry, instance })) + ); + + // TODO: clean up unneccessary global events + on( + globalAppRegistry, + 'open-instance-workspace', + (workspace?: 'My Queries' | 'Databases' | 'Performance') => { + store.dispatch(openWorkspace({ type: workspace ?? 'My Queries' })); + } + ); + + on(globalAppRegistry, 'select-database', (namespace: string) => { + store.dispatch(openWorkspace({ type: 'Collections', namespace })); + }); + + const openCollection = ( + metadata: CollectionTabPluginMetadata, + newTab: boolean + ) => { + store.dispatch( + openWorkspace( + { + type: 'Collection', + ...metadata, + }, + { newTab } + ) + ); + }; + + on( + globalAppRegistry, + 'open-namespace-in-new-tab', + (metadata: CollectionTabPluginMetadata) => { + openCollection(metadata, true); + } + ); + + on( + globalAppRegistry, + 'select-namespace', + (metadata: CollectionTabPluginMetadata) => { + openCollection(metadata, false); + } + ); + + on( + globalAppRegistry, + 'collection-renamed', + ({ from, to }: { from: string; to: string }) => { + store.dispatch(collectionRenamed(from, to)); + } + ); + + on(instance, 'remove:collections', (collection: Collection) => { + store.dispatch(collectionRemoved(collection.ns)); + }); + + on(instance, 'remove:databases', (database: Database) => { + store.dispatch(databaseRemoved(database.name)); + }); + + on(globalAppRegistry, 'menu-share-schema-json', () => { + const activeTab = getActiveTab(store.getState()); + if (!activeTab) return; + getLocalAppRegistryForTab(activeTab.id).emit('menu-share-schema-json'); + }); + + on(globalAppRegistry, 'open-active-namespace-export', function () { + const activeTab = getActiveTab(store.getState()); + if (!activeTab) return; + globalAppRegistry.emit('open-export', { + exportFullCollection: true, + namespace: activeTab.namespace, + origin: 'menu', + }); + }); + + on(globalAppRegistry, 'open-active-namespace-import', function () { + const activeTab = getActiveTab(store.getState()); + if (!activeTab) return; + globalAppRegistry.emit('open-import', { + namespace: activeTab.namespace, + origin: 'menu', + }); + }); + + return { + store, + deactivate: cleanup, + }; +} + +const WorkspacesPlugin = registerHadronPlugin( + { + name: 'Workspaces', + component: Workspaces, + activate: activateWorkspacePlugin, + }, + { instance: mongoDBInstanceLocator } +); + +function activate(): void { + // noop +} + +function deactivate(): void { + // noop +} + +export default WorkspacesPlugin; +export { activate, deactivate }; +export { default as metadata } from '../package.json'; +export { WorkspacesProvider } from './components/workspaces-provider'; diff --git a/packages/compass-workspaces/src/stores/workspaces.spec.ts b/packages/compass-workspaces/src/stores/workspaces.spec.ts new file mode 100644 index 00000000000..2f620b9d3b1 --- /dev/null +++ b/packages/compass-workspaces/src/stores/workspaces.spec.ts @@ -0,0 +1,276 @@ +import { expect } from 'chai'; +import { activateWorkspacePlugin } from '../index'; +import * as workspacesSlice from './workspaces'; + +describe('tabs behavior', function () { + const instance = { on() {}, removeListener() {} } as any; + const globalAppRegistry = { on() {}, removeListener() {} } as any; + const helpers = { on() {}, cleanup() {} } as any; + + function configureStore() { + return activateWorkspacePlugin({}, { globalAppRegistry, instance }, helpers) + .store; + } + + function openTabs( + store: ReturnType, + namespaces: string[] = ['test.foo', 'test.bar', 'test.buz'] + ) { + namespaces.forEach((namespace) => { + store.dispatch( + openWorkspace({ type: 'Collection', namespace } as any, { + newTab: true, + }) + ); + }); + } + + const { + openWorkspace, + openTabFromCurrent, + selectTab, + selectNextTab, + selectPrevTab, + moveTab, + closeTab, + collectionRenamed, + collectionRemoved, + databaseRemoved, + } = workspacesSlice; + + describe('openWorkspace', function () { + it('should open a tab and make it active', function () { + const store = configureStore(); + store.dispatch(openWorkspace({ type: 'My Queries' })); + const state = store.getState(); + expect(state).to.have.property('tabs').have.lengthOf(1); + expect(state).to.have.nested.property('tabs[0].type', 'My Queries'); + expect(state).to.have.property('activeTabId', state.tabs[0].id); + }); + + it('should open a workspace in new tab even if another exists', function () { + const store = configureStore(); + store.dispatch(openWorkspace({ type: 'My Queries' })); + store.dispatch(openWorkspace({ type: 'My Queries' }, { newTab: true })); + const state = store.getState(); + expect(state).to.have.property('tabs').have.lengthOf(2); + expect(state).to.have.nested.property('tabs[0].type', 'My Queries'); + expect(state).to.have.nested.property('tabs[1].type', 'My Queries'); + expect(state).to.have.property('activeTabId', state.tabs[1].id); + }); + + it('should select already opened tab when trying to open a new one with the same attributes', function () { + const store = configureStore(); + openTabs(store); + const currentState = store.getState(); + + // opening literally the same tab, state is not changed + store.dispatch( + openWorkspace({ type: 'Collection', namespace: 'test.buz' } as any) + ); + expect(store.getState()).to.eq(currentState); + + // opening an existing tab changes the active id, but doesn't change the + // tabs array + store.dispatch( + openWorkspace({ type: 'Collection', namespace: 'test.foo' } as any) + ); + expect(store.getState()).to.have.property('tabs', currentState.tabs); + expect(store.getState()).to.have.property( + 'activeTabId', + currentState.tabs[0].id + ); + }); + + it('should not change any state when opening a workspace for the active tab even if other similar workspaces are open', function () { + const store = configureStore(); + openTabs(store, ['db.coll', 'db.coll', 'db.coll']); + const currentState = store.getState(); + store.dispatch( + openWorkspace({ type: 'Collection', namespace: 'db.coll' } as any) + ); + expect(store.getState()).to.eq(currentState); + }); + }); + + describe('openTabFromCurrent', function () { + it('should open a tab that copies current active tab', function () { + const store = configureStore(); + openTabs(store); + const currentActiveTab = workspacesSlice.getActiveTab(store.getState()); + store.dispatch(openTabFromCurrent()); + const state = store.getState(); + const newActiveTab = workspacesSlice.getActiveTab(state); + expect(newActiveTab).to.not.eq(currentActiveTab); + expect(state).to.have.property('tabs').have.lengthOf(4); + expect(state).to.have.nested.property( + 'tabs[3].namespace', + currentActiveTab?.namespace + ); + expect(state).to.have.property('activeTabId', newActiveTab?.id); + }); + }); + + describe('selectTab', function () { + it('should select tab by index', function () { + const store = configureStore(); + openTabs(store); + store.dispatch(selectTab(0)); + const state = store.getState(); + expect(state).to.have.property('activeTabId', state.tabs[0].id); + store.dispatch(selectTab(0)); + expect(store.getState()).to.eq(state); + }); + }); + + describe('selectNextTab', function () { + it('should select next tab', function () { + const store = configureStore(); + openTabs(store); + store.dispatch(selectNextTab()); + const state1 = store.getState(); + expect(state1).to.have.property('activeTabId', state1.tabs[0].id); + store.dispatch(selectNextTab()); + const state2 = store.getState(); + expect(state2).to.have.property('activeTabId', state2.tabs[1].id); + store.dispatch(selectNextTab()); + const state3 = store.getState(); + expect(state3).to.have.property('activeTabId', state3.tabs[2].id); + store.dispatch(selectNextTab()); + const state4 = store.getState(); + expect(state4).to.have.property('activeTabId', state4.tabs[0].id); + }); + }); + + describe('selectPrevTab', function () { + it('should select previous tab', function () { + const store = configureStore(); + openTabs(store); + store.dispatch(selectPrevTab()); + const state1 = store.getState(); + expect(state1).to.have.property('activeTabId', state1.tabs[1].id); + store.dispatch(selectPrevTab()); + const state2 = store.getState(); + expect(state2).to.have.property('activeTabId', state2.tabs[0].id); + store.dispatch(selectPrevTab()); + const state3 = store.getState(); + expect(state3).to.have.property('activeTabId', state3.tabs[2].id); + store.dispatch(selectPrevTab()); + const state4 = store.getState(); + expect(state4).to.have.property('activeTabId', state4.tabs[1].id); + }); + }); + + describe('moveTab', function () { + it('should move tab from one index to another and preserve active tab id', function () { + const store = configureStore(); + openTabs(store); + const currentActiveTab = workspacesSlice.getActiveTab(store.getState()); + store.dispatch(moveTab(2, 0)); + const state = store.getState(); + expect(state).to.have.property('activeTabId', currentActiveTab?.id); + expect(state).to.have.nested.property('tabs[0]', currentActiveTab); + }); + }); + + describe('closeTab', function () { + it('should close tab and make another tab active if needed', function () { + const store = configureStore(); + openTabs(store); + const currentActiveTab = workspacesSlice.getActiveTab(store.getState()); + // closing inactive tab + store.dispatch(closeTab(0)); + const state1 = store.getState(); + expect(state1).to.have.property('tabs').have.lengthOf(2); + // active tab didn't change + expect(state1).to.have.property('activeTabId', currentActiveTab?.id); + // closing active tab + store.dispatch(closeTab(1)); + const state2 = store.getState(); + expect(state2).to.have.property('tabs').have.lengthOf(1); + // another tab was selected + expect(state2) + .to.have.property('activeTabId') + .not.eq(currentActiveTab?.id); + }); + }); + + describe('collectionRenamed', function () { + it('should not change state if no tabs were renamed', function () { + const store = configureStore(); + openTabs(store); + const state = store.getState(); + store.dispatch(collectionRenamed('foo.bar', 'foo.buz')); + expect(store.getState()).to.eq(state); + }); + + it('should replace applicable tabs with the new namespace', function () { + const store = configureStore(); + openTabs(store); + const tabToRename = store + .getState() + .tabs.find((tab) => tab.namespace === 'test.foo'); + store.dispatch(collectionRenamed('test.foo', 'test.new-foo')); + const state = store.getState(); + expect(state.tabs.find((tab) => tab.namespace === 'test.foo')).to.not + .exist; + const renamed = state.tabs.find( + (tab) => tab.namespace === 'test.new-foo' + ); + expect(renamed).to.exist; + expect(renamed).to.not.eq(tabToRename); + }); + }); + + describe('collectionRemoved', function () { + it('should not change state if no tabs were removed', function () { + const store = configureStore(); + openTabs(store); + const state = store.getState(); + store.dispatch(collectionRenamed('foo.bar', 'foo.buz')); + expect(store.getState()).to.eq(state); + }); + + it('should remove all tabs with matching namespace', function () { + const store = configureStore(); + openTabs(store); + store.dispatch(collectionRemoved('test.foo')); + const state = store.getState(); + expect(state).to.have.property('tabs').have.lengthOf(2); + expect(state.tabs.find((tab) => tab.namespace === 'test.foo')).to.not + .exist; + }); + }); + + describe('databaseRemoved', function () { + it('should not change state if no tabs were removed', function () { + const store = configureStore(); + openTabs(store); + const state = store.getState(); + store.dispatch(collectionRenamed('foo.bar', 'foo.buz')); + expect(store.getState()).to.eq(state); + }); + + it('should remove all tabs with matching namespace', function () { + const store = configureStore(); + openTabs(store); + store.dispatch(openWorkspace({ type: 'Collections', namespace: 'test' })); + store.dispatch(openWorkspace({ type: 'My Queries' })); + const myQueriesTab = workspacesSlice.getActiveTab(store.getState()); + store.dispatch(databaseRemoved('test')); + const state = store.getState(); + expect(state).to.have.property('tabs').have.lengthOf(1); + expect(state).to.have.property('tabs').deep.eq([myQueriesTab]); + }); + + it('should remove all tabs completely if all of them match namespace', function () { + const store = configureStore(); + openTabs(store); + store.dispatch(databaseRemoved('test')); + const state = store.getState(); + expect(state).to.have.property('tabs').have.lengthOf(0); + expect(state).to.have.property('tabs').deep.eq([]); + expect(state).to.have.property('activeTabId', null); + }); + }); +}); diff --git a/packages/compass-workspaces/src/stores/workspaces.ts b/packages/compass-workspaces/src/stores/workspaces.ts new file mode 100644 index 00000000000..6c33dcde21c --- /dev/null +++ b/packages/compass-workspaces/src/stores/workspaces.ts @@ -0,0 +1,529 @@ +import type { Reducer, AnyAction } from 'redux'; +import type { ThunkAction } from 'redux-thunk'; +import { ObjectId } from 'bson'; +import AppRegistry from 'hadron-app-registry'; +import toNS from 'mongodb-ns'; +import type { WorkspacesServices } from '..'; +import type { MyQueriesWorkspace } from '@mongodb-js/compass-saved-aggregations-queries'; +import type { ServerStatsWorkspace } from '@mongodb-js/compass-serverstats'; +import type { + DatabasesWorkspace, + CollectionsWorkspace, +} from '@mongodb-js/compass-databases-collections'; +import type { CollectionWorkspace } from '@mongodb-js/compass-collection'; +import { isEqual } from 'lodash'; + +export type AnyWorkspace = + | MyQueriesWorkspace + | ServerStatsWorkspace + | DatabasesWorkspace + | CollectionsWorkspace + | CollectionWorkspace; + +export type Workspace = Extract< + AnyWorkspace, + { type: T } +>; + +const LocalAppRegistryMap = new Map(); + +type WorkspacesThunkAction = ThunkAction< + R, + WorkspacesState, + WorkspacesServices, + A +>; + +export const getLocalAppRegistryForTab = (tabId: string) => { + let appRegistry = LocalAppRegistryMap.get(tabId); + if (appRegistry) { + return appRegistry; + } + appRegistry = new AppRegistry(); + LocalAppRegistryMap.set(tabId, appRegistry); + return appRegistry; +}; + +const cleanupLocalAppRegistryForTab = (tabId: string): boolean => { + const appRegistry = LocalAppRegistryMap.get(tabId); + appRegistry?.deactivate(); + return LocalAppRegistryMap.delete(tabId); +}; + +export enum WorkspacesActions { + OpenWorkspace = 'compass-workspaces/OpenWorkspace', + SelectTab = 'compass-workspaces/SelectTab', + SelectPreviousTab = 'compass-workspaces/SelectPreviousTab', + SelectNextTab = 'compass-workspaces/SelectNextTab', + MoveTab = 'compass-workspaces/MoveTab', + OpenTabFromCurrentActive = 'compass-workspaces/OpenTabFromCurrentActive', + CloseTab = 'compass-workspaces/CloseTab', + CollectionRenamed = 'compass-workspaces/CollectionRenamed', + CollectionRemoved = 'compass-workspaces/CollectionRemoved', + DatabaseRemoved = 'compass-workspaces/DatabaseRemoved', +} + +function isAction( + action: AnyAction, + type: A['type'] +): action is A { + return action.type === type; +} + +export type WorkspaceTab = { id: string } & AnyWorkspace; + +export type WorkspacesState = { + tabs: WorkspaceTab[]; + activeTabId: string | null; +}; + +const getTabId = () => { + return new ObjectId().toString(); +}; + +export const getInitialTabState = ( + workspace: OpenWorkspaceOptions +): WorkspaceTab => { + const tabId = getTabId(); + return { id: tabId, ...workspace } as WorkspaceTab; +}; + +const getInitialState = () => { + return { + tabs: [] as WorkspaceTab[], + activeTabId: null, + }; +}; + +const reducer: Reducer = ( + state = getInitialState(), + action +) => { + if (isAction(action, WorkspacesActions.OpenWorkspace)) { + if (action.newTab) { + const newTab = getInitialTabState(action.workspace); + return { + tabs: [...state.tabs, newTab], + activeTabId: newTab.id, + }; + } + const activeTab = getActiveTab(state); + const existingTab = + // If there is an active tab, give it priority when looking for a tab to + // select when opening a tab, it might be that we don't need to update the + // state at all + (activeTab ? [activeTab, ...state.tabs] : state.tabs).find( + ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + id: _id, + ...tab + }) => { + return isEqual(tab, action.workspace); + } + ); + if (existingTab) { + if (existingTab.id !== state.activeTabId) { + return { + ...state, + activeTabId: existingTab.id, + }; + } + return state; + } + // If there is no existing tab matching the one we're trying to open, either + // replace the current tab if we're trying to open the same workspace that + // is currently active, or just open a new tab with the workspace + const newTab = getInitialTabState(action.workspace); + if (activeTab?.type !== action.workspace.type) { + return { + tabs: [...state.tabs, newTab], + activeTabId: newTab.id, + }; + } + const activeTabIndex = getActiveTabIndex(state); + const newTabs = [...state.tabs]; + newTabs.splice(activeTabIndex, 1, newTab); + return { + tabs: newTabs, + activeTabId: newTab.id, + }; + } + + if ( + isAction( + action, + WorkspacesActions.OpenTabFromCurrentActive + ) + ) { + const currentActiveTab = getActiveTab(state); + let newTab: WorkspaceTab; + if (!currentActiveTab) { + newTab = getInitialTabState({ type: 'My Queries' }); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _id, ...tabProps } = currentActiveTab; + newTab = getInitialTabState(tabProps); + } + return { + tabs: [...state.tabs, newTab], + activeTabId: newTab.id, + }; + } + + if (isAction(action, WorkspacesActions.SelectTab)) { + if (state.tabs[action.atIndex]?.id === state.activeTabId) { + return state; + } + return { + ...state, + activeTabId: state.tabs[action.atIndex]?.id ?? null, + }; + } + + if (isAction(action, WorkspacesActions.SelectNextTab)) { + const newActiveTabIndex = + (getActiveTabIndex(state) + 1) % state.tabs.length; + const newActiveTab = state.tabs[newActiveTabIndex]; + if (newActiveTab?.id === state.activeTabId) { + return state; + } + return { + ...state, + activeTabId: newActiveTab?.id ?? state.activeTabId, + }; + } + + if ( + isAction( + action, + WorkspacesActions.SelectPreviousTab + ) + ) { + const currentActiveTabIndex = getActiveTabIndex(state); + const newActiveTabIndex = + getActiveTabIndex(state) === 0 + ? state.tabs.length - 1 + : currentActiveTabIndex - 1; + const newActiveTab = state.tabs[newActiveTabIndex]; + if (newActiveTab?.id === state.activeTabId) { + return state; + } + return { + ...state, + activeTabId: newActiveTab?.id ?? state.activeTabId, + }; + } + + if (isAction(action, WorkspacesActions.MoveTab)) { + if (action.fromIndex === action.toIndex) { + return state; + } + const newTabs = [...state.tabs]; + newTabs.splice(action.toIndex, 0, newTabs.splice(action.fromIndex, 1)[0]); + return { + ...state, + tabs: newTabs, + }; + } + + if (isAction(action, WorkspacesActions.CloseTab)) { + const tabToClose = state.tabs[action.atIndex]; + const tabIndex = state.tabs.findIndex((tab) => tab.id === tabToClose?.id); + const newTabs = [...state.tabs]; + newTabs.splice(action.atIndex, 1); + const newActiveTabId = + tabToClose.id === state.activeTabId + ? // We follow standard browser behavior with tabs on how we handle + // which tab gets activated if we close the active tab. If the active + // tab is the last tab, we activate the one before it, otherwise we + // activate the next tab. + (state.tabs[tabIndex + 1] ?? newTabs[newTabs.length - 1])?.id ?? null + : state.activeTabId; + return { + activeTabId: newActiveTabId, + tabs: newTabs, + }; + } + + if ( + isAction( + action, + WorkspacesActions.CollectionRemoved + ) + ) { + const tabs = state.tabs.filter((tab) => { + switch (tab.type) { + case 'Collection': + return tab.namespace !== action.namespace; + default: + return true; + } + }); + if (tabs.length === state.tabs.length) { + return state; + } + const activeTabRemoved = !tabs.some((tab) => { + return tab.id === state.activeTabId; + }); + return { + tabs, + activeTabId: activeTabRemoved + ? tabs[tabs.length - 1]?.id ?? null + : state.activeTabId, + }; + } + + if ( + isAction(action, WorkspacesActions.DatabaseRemoved) + ) { + const tabs = state.tabs.filter((tab) => { + switch (tab.type) { + case 'Collections': + return tab.namespace !== action.namespace; + case 'Collection': + return toNS(tab.namespace).database !== action.namespace; + default: + return true; + } + }); + if (tabs.length === state.tabs.length) { + return state; + } + const activeTabRemoved = !tabs.some((tab) => { + return tab.id === state.activeTabId; + }); + return { + tabs, + activeTabId: activeTabRemoved + ? tabs[tabs.length - 1]?.id ?? null + : state.activeTabId, + }; + } + + if ( + isAction( + action, + WorkspacesActions.CollectionRenamed + ) + ) { + let tabsRenamed = 0; + let newActiveTabId = state.activeTabId; + const newTabs = state.tabs.map((tab) => { + if (tab.type === 'Collection' && tab.namespace === action.from) { + tabsRenamed++; + const { id, ...workspace } = tab; + const newTab = getInitialTabState({ + ...workspace, + namespace: action.to, + }); + if (id === state.activeTabId) { + newActiveTabId = newTab.id; + } + return newTab; + } + return tab; + }); + if (tabsRenamed === 0) { + return state; + } + return { + tabs: newTabs, + activeTabId: newActiveTabId, + }; + } + + return state; +}; + +export const getActiveTabIndex = (state: WorkspacesState) => { + const { activeTabId, tabs } = state; + return tabs.findIndex((tab) => tab.id === activeTabId); +}; + +export const getActiveTab = (state: WorkspacesState): WorkspaceTab | null => { + return state.tabs[getActiveTabIndex(state)] ?? null; +}; + +export type OpenWorkspaceOptions = + | Pick, 'type'> + | Pick, 'type'> + | Pick, 'type'> + | Pick, 'type' | 'namespace'> + + // TODO: for now opening a collection workspace requires all metadata to be + // passed with it, this will change when collection tab is responsible for + // fetching its own metadata + // + // | (Pick, 'type' | 'namespace'> & + // Partial< + // Pick< + // Workspace<'Collection'>, + // 'query' | 'aggregation' | 'pipelineText' | 'editViewName' + // > + // >); + | Workspace<'Collection'>; + +type OpenWorkspaceAction = { + type: WorkspacesActions.OpenWorkspace; + workspace: OpenWorkspaceOptions; + newTab?: boolean; +}; + +export const openWorkspace = ( + workspaceOptions: OpenWorkspaceOptions, + tabOptions?: { newTab?: boolean } +): OpenWorkspaceAction => { + return { + type: WorkspacesActions.OpenWorkspace, + workspace: workspaceOptions, + newTab: !!tabOptions?.newTab, + }; +}; + +type SelectTabAction = { type: WorkspacesActions.SelectTab; atIndex: number }; + +export const selectTab = (atIndex: number): SelectTabAction => { + return { type: WorkspacesActions.SelectTab, atIndex }; +}; + +type MoveTabAction = { + type: WorkspacesActions.MoveTab; + fromIndex: number; + toIndex: number; +}; + +export const moveTab = (fromIndex: number, toIndex: number): MoveTabAction => { + return { type: WorkspacesActions.MoveTab, fromIndex, toIndex }; +}; + +type SelectPreviousTabAction = { type: WorkspacesActions.SelectPreviousTab }; + +export const selectPrevTab = (): SelectPreviousTabAction => { + return { type: WorkspacesActions.SelectPreviousTab }; +}; + +type SelectNextTabAction = { type: WorkspacesActions.SelectNextTab }; + +export const selectNextTab = (): SelectNextTabAction => { + return { type: WorkspacesActions.SelectNextTab }; +}; + +type OpenTabFromCurrentActiveAction = { + type: WorkspacesActions.OpenTabFromCurrentActive; +}; + +export const openTabFromCurrent = (): OpenTabFromCurrentActiveAction => { + return { type: WorkspacesActions.OpenTabFromCurrentActive }; +}; + +type CloseTabAction = { type: WorkspacesActions.CloseTab; atIndex: number }; + +export const closeTab = ( + atIndex: number +): WorkspacesThunkAction => { + return (dispatch, getState) => { + const tab = getState().tabs[atIndex]; + dispatch({ type: WorkspacesActions.CloseTab, atIndex }); + cleanupLocalAppRegistryForTab(tab?.id); + }; +}; + +type CollectionRenamedAction = { + type: WorkspacesActions.CollectionRenamed; + from: string; + to: string; +}; + +export const collectionRenamed = ( + from: string, + to: string +): WorkspacesThunkAction => { + return (dispatch, getState) => { + const tabsToReplace = getState().tabs.filter( + (tab) => tab.type === 'Collection' && tab.namespace === from + ); + dispatch({ type: WorkspacesActions.CollectionRenamed, from, to }); + tabsToReplace.forEach((tab) => { + cleanupLocalAppRegistryForTab(tab.id); + }); + }; +}; + +type CollectionRemovedAction = { + type: WorkspacesActions.CollectionRemoved; + namespace: string; +}; + +export const collectionRemoved = ( + namespace: string +): WorkspacesThunkAction => { + return (dispatch, getState) => { + const tabsToRemove = getState().tabs.filter( + (tab) => tab.type === 'Collection' && tab.namespace === namespace + ); + dispatch({ + type: WorkspacesActions.CollectionRemoved, + namespace, + }); + tabsToRemove.forEach((tab) => { + cleanupLocalAppRegistryForTab(tab.id); + }); + }; +}; + +type DatabaseRemovedAction = { + type: WorkspacesActions.DatabaseRemoved; + namespace: string; +}; + +export const databaseRemoved = ( + namespace: string +): WorkspacesThunkAction => { + return (dispatch, getState) => { + const tabsToRemove = getState().tabs.filter((tab) => { + switch (tab.type) { + case 'Collections': + return tab.namespace === namespace; + case 'Collection': + return toNS(tab.namespace).database === namespace; + default: + return false; + } + }); + dispatch({ + type: WorkspacesActions.DatabaseRemoved, + namespace, + }); + tabsToRemove.forEach((tab) => { + cleanupLocalAppRegistryForTab(tab.id); + }); + }; +}; + +// TODO: events are re-emitted when tab is changed for compatibility reasons, +// this will go away as soon as we finish the compass-workspaces refactor. +// Emitting these event will trigger store actions for opening a tab, but they +// will be no-ops, will not result in any state update +export const emitOnTabChange = ( + newTab: WorkspaceTab +): WorkspacesThunkAction => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _id, ...tabMeta } = newTab; + return (_dispatch, _getState, { globalAppRegistry }) => { + switch (tabMeta.type) { + case 'My Queries': + case 'Databases': + case 'Performance': + globalAppRegistry.emit('open-instance-workspace', tabMeta.type); + return; + case 'Collections': + globalAppRegistry.emit('select-database', tabMeta.namespace); + return; + case 'Collection': + globalAppRegistry.emit('select-namespace', tabMeta); + return; + } + }; +}; + +export default reducer; diff --git a/packages/compass-database/tsconfig-lint.json b/packages/compass-workspaces/tsconfig-lint.json similarity index 100% rename from packages/compass-database/tsconfig-lint.json rename to packages/compass-workspaces/tsconfig-lint.json diff --git a/packages/compass-instance/tsconfig.json b/packages/compass-workspaces/tsconfig.json similarity index 79% rename from packages/compass-instance/tsconfig.json rename to packages/compass-workspaces/tsconfig.json index e45df8e2f65..79bc84584ce 100644 --- a/packages/compass-instance/tsconfig.json +++ b/packages/compass-workspaces/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "@mongodb-js/tsconfig-compass/tsconfig.react.json", "compilerOptions": { - "outDir": "dist", - "allowJs": true + "outDir": "dist" }, "include": ["src/**/*"], "exclude": ["./src/**/*.spec.*"] diff --git a/packages/compass-database/webpack.config.js b/packages/compass-workspaces/webpack.config.js similarity index 100% rename from packages/compass-database/webpack.config.js rename to packages/compass-workspaces/webpack.config.js diff --git a/packages/compass/package.json b/packages/compass/package.json index 8924510c602..7e248747d47 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -176,14 +176,13 @@ "system-ca": "^1.0.2" }, "devDependencies": { - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "@electron/remote": "^2.1.0", "@mongodb-js/atlas-service": "^0.10.1", "@mongodb-js/compass-aggregations": "^9.21.0", "@mongodb-js/compass-app-stores": "^7.6.1", "@mongodb-js/compass-collection": "^4.19.1", "@mongodb-js/compass-crud": "^13.20.0", - "@mongodb-js/compass-database": "^3.19.1", "@mongodb-js/compass-databases-collections": "^1.19.1", "@mongodb-js/compass-explain-plan": "^6.20.0", "@mongodb-js/compass-export-to-language": "^8.21.0", @@ -192,7 +191,6 @@ "@mongodb-js/compass-home": "^6.20.1", "@mongodb-js/compass-import-export": "^7.19.1", "@mongodb-js/compass-indexes": "^5.19.1", - "@mongodb-js/compass-instance": "^4.19.1", "@mongodb-js/compass-logging": "^1.2.6", "@mongodb-js/compass-query-bar": "^8.20.0", "@mongodb-js/compass-saved-aggregations-queries": "^1.20.1", @@ -223,7 +221,7 @@ "compass-preferences-model": "^2.15.6", "debug": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^25.9.6", + "electron": "^25.9.7", "electron-devtools-installer": "^3.2.0", "electron-dl": "^3.5.0", "electron-mocha": "^10.1.0", diff --git a/packages/compass/src/app/plugins/default.js b/packages/compass/src/app/plugins/default.js index b5a27e52c48..e331bacce5f 100644 --- a/packages/compass/src/app/plugins/default.js +++ b/packages/compass/src/app/plugins/default.js @@ -4,13 +4,11 @@ module.exports = [ require('@mongodb-js/compass-export-to-language'), require('@mongodb-js/compass-collection'), require('@mongodb-js/compass-crud'), - require('@mongodb-js/compass-database'), require('@mongodb-js/compass-databases-collections'), require('@mongodb-js/compass-field-store'), require('@mongodb-js/compass-find-in-page'), require('@mongodb-js/compass-home'), require('@mongodb-js/compass-import-export'), - require('@mongodb-js/compass-instance'), require('@mongodb-js/compass-query-bar'), require('@mongodb-js/compass-schema'), require('@mongodb-js/compass-schema-validation'), diff --git a/packages/connection-storage/package.json b/packages/connection-storage/package.json index 83c15f3db1b..236dc378255 100644 --- a/packages/connection-storage/package.json +++ b/packages/connection-storage/package.json @@ -56,7 +56,7 @@ "@mongodb-js/compass-user-data": "^0.1.9", "@mongodb-js/compass-utils": "^0.5.5", "bson": "^6.2.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "hadron-ipc": "^3.2.4", "keytar": "^7.9.0", "lodash": "^4.17.21", diff --git a/packages/databases-collections/src/collections-plugin.jsx b/packages/databases-collections/src/collections-plugin.tsx similarity index 73% rename from packages/databases-collections/src/collections-plugin.jsx rename to packages/databases-collections/src/collections-plugin.tsx index a39563bf8f0..c618ac43316 100644 --- a/packages/databases-collections/src/collections-plugin.jsx +++ b/packages/databases-collections/src/collections-plugin.tsx @@ -4,13 +4,15 @@ import { Provider } from 'react-redux'; import CollectionsPlugin from './components/collections'; import store from './stores/collections-store'; -class Plugin extends Component { +class Plugin extends Component<{ + // TODO: not currently used, but will be when the plugin is converted to the + // new interface + namespace: string; +}> { static displayName = 'CollectionsPlugin'; /** * Connect the Plugin to the store and render. - * - * @returns {React.Component} The rendered component. */ render() { return ( diff --git a/packages/databases-collections/src/components/databases/databases.tsx b/packages/databases-collections/src/components/databases/databases.tsx index 73d908c3d9f..41e52a297c7 100644 --- a/packages/databases-collections/src/components/databases/databases.tsx +++ b/packages/databases-collections/src/components/databases/databases.tsx @@ -18,6 +18,7 @@ import { refreshDatabases, selectDatabase, } from '../../modules/databases'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const errorContainerStyles = css({ padding: spacing[3], @@ -83,6 +84,14 @@ const Databases: React.FunctionComponent = ({ }) => { const isPreferencesReadOnly = usePreference('readOnly', React); + useTrackOnChange( + 'COMPASS-DATABASES-UI', + (track) => { + track('Screen', { name: 'databases' }); + }, + [] + ); + if (databasesLoadingStatus === 'error') { return (
@@ -137,7 +146,7 @@ const mapDispatchToProps = { const ConnectedDatabases = connect( mapStateToProps, mapDispatchToProps -)(Databases); +)(Databases) as React.FunctionComponent>; export default ConnectedDatabases; export { Databases }; diff --git a/packages/databases-collections/src/index.ts b/packages/databases-collections/src/index.ts index 494e6442f0a..230dcd25770 100644 --- a/packages/databases-collections/src/index.ts +++ b/packages/databases-collections/src/index.ts @@ -18,18 +18,24 @@ import { DatabasesPlugin } from './databases-plugin'; import MappedRenameCollectionModal from './components/rename-collection-modal/rename-collection-modal'; import { activateRenameCollectionPlugin } from './stores/rename-collection'; -// View collections list plugin. -const COLLECTIONS_PLUGIN_ROLE = { - name: 'Collections', +export const CollectionsWorkspaceTab = { + name: 'Collections' as const, component: CollectionsPlugin, - order: 1, }; -export const InstanceTab = { - name: 'Databases', +export type CollectionsWorkspace = { + type: typeof CollectionsWorkspaceTab['name']; +} & React.ComponentProps; + +export const DatabasesWorkspaceTab = { + name: 'Databases' as const, component: DatabasesPlugin, }; +export type DatabasesWorkspace = { + type: typeof DatabasesWorkspaceTab['name']; +} & React.ComponentProps; + export const CreateNamespacePlugin = registerHadronPlugin( { name: 'CreateNamespace', @@ -75,7 +81,6 @@ export const RenameCollectionPlugin = registerHadronPlugin( * Activate all the components in the package. **/ function activate(appRegistry: AppRegistry) { - appRegistry.registerRole('Database.Tab', COLLECTIONS_PLUGIN_ROLE); appRegistry.registerStore( 'CollectionsPlugin.CollectionsStore', CollectionsStore @@ -86,7 +91,6 @@ function activate(appRegistry: AppRegistry) { * Deactivate all the components in the package. **/ function deactivate(appRegistry: AppRegistry) { - appRegistry.deregisterRole('Database.Tab', COLLECTIONS_PLUGIN_ROLE); appRegistry.deregisterStore('CollectionsPlugin.CollectionsStore'); } diff --git a/packages/databases-collections/src/stores/collections-store.js b/packages/databases-collections/src/stores/collections-store.js index 455eeb78752..29ca5e7b9b9 100644 --- a/packages/databases-collections/src/stores/collections-store.js +++ b/packages/databases-collections/src/stores/collections-store.js @@ -1,7 +1,6 @@ import throttle from 'lodash/throttle'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; -import toNS from 'mongodb-ns'; import { appRegistryActivated } from '../modules/app-registry'; import { changeDatabaseName } from '../modules/database-name'; import { dataServiceConnected } from '../modules/data-service'; @@ -127,22 +126,6 @@ store.onActivated = (appRegistry) => { store.dispatch(dataServiceConnected(error, dataService)); }); - appRegistry.on('collection-dropped', (namespace) => { - const { database } = toNS(namespace); - - const currentDatabase = store.getState().databaseName; - if (database === currentDatabase) { - appRegistry.emit('active-collection-dropped', namespace); - } - }); - - appRegistry.on('database-dropped', (name) => { - const currentDatabase = store.getState().databaseName; - if (name === currentDatabase) { - appRegistry.emit('active-database-dropped', name); - } - }); - /** * Set the app registry to use later. */ diff --git a/packages/databases-collections/src/stores/create-namespace.spec.tsx b/packages/databases-collections/src/stores/create-namespace.spec.tsx index ac8baa8a46e..1ecfd5630eb 100644 --- a/packages/databases-collections/src/stores/create-namespace.spec.tsx +++ b/packages/databases-collections/src/stores/create-namespace.spec.tsx @@ -22,6 +22,7 @@ describe('CreateNamespacePlugin', function () { const instance = { on: sandbox.stub(), off: sandbox.stub(), + removeListener: sandbox.stub(), build: { version: '999.999.999' }, topologyDescription: { type: 'Unknown' }, }; diff --git a/packages/databases-collections/src/stores/databases-store.ts b/packages/databases-collections/src/stores/databases-store.ts index 6330ada60ba..d2160d68b52 100644 --- a/packages/databases-collections/src/stores/databases-store.ts +++ b/packages/databases-collections/src/stores/databases-store.ts @@ -14,7 +14,7 @@ type DatabasesTabServices = { }; export function activatePlugin( - _: unknown, + _initialProps: Record, { globalAppRegistry, instance }: DatabasesTabServices ) { const store = createStore( diff --git a/packages/hadron-app-registry/src/index.ts b/packages/hadron-app-registry/src/index.ts index 3508aee55d6..59ffdd2ce17 100644 --- a/packages/hadron-app-registry/src/index.ts +++ b/packages/hadron-app-registry/src/index.ts @@ -6,12 +6,11 @@ export { AppRegistryProvider, useGlobalAppRegistry, useLocalAppRegistry, - useAppRegistryComponent, - useAppRegistryRole, } from './react-context'; export type { HadronPluginComponent, HadronPluginConfig, + ActivateHelpers, } from './register-plugin'; export { registerHadronPlugin } from './register-plugin'; export default AppRegistry; diff --git a/packages/hadron-app-registry/src/react-context.tsx b/packages/hadron-app-registry/src/react-context.tsx index 2f8c3b410aa..ad406a8372e 100644 --- a/packages/hadron-app-registry/src/react-context.tsx +++ b/packages/hadron-app-registry/src/react-context.tsx @@ -6,8 +6,6 @@ import React, { useState, } from 'react'; import { globalAppRegistry, AppRegistry } from './app-registry'; -import createDebug from 'debug'; -const debug = createDebug('hadron-app-registry:react'); /** * @internal exported for the mock plugin helper implementation @@ -105,42 +103,3 @@ export function useLocalAppRegistry(): AppRegistry { } return appRegistry; } - -/** @deprecated prefer using plugins or direct references instead */ -export function useAppRegistryComponent( - componentName: string -): React.JSXElementConstructor | null { - const appRegistry = useGlobalAppRegistry(); - - const [component] = useState(() => { - const newComponent = appRegistry.getComponent(componentName); - if (!newComponent) { - debug( - `home plugin loading component, but ${String(componentName)} is NULL` - ); - } - return newComponent; - }); - - return component ? component : null; -} - -/** @deprecated prefer using plugins or direct references instead */ -export function useAppRegistryRole(roleName: string): - | { - component: React.JSXElementConstructor; - name: string; - }[] - | null { - const appRegistry = useGlobalAppRegistry(); - - const [role] = useState(() => { - const newRole = appRegistry.getRole(roleName); - if (!newRole) { - debug(`home plugin loading role, but ${String(roleName)} is NULL`); - } - return newRole; - }); - - return role ? role : null; -} diff --git a/packages/hadron-app-registry/src/register-plugin.tsx b/packages/hadron-app-registry/src/register-plugin.tsx index 4a3c958c83b..9d715d6d951 100644 --- a/packages/hadron-app-registry/src/register-plugin.tsx +++ b/packages/hadron-app-registry/src/register-plugin.tsx @@ -10,6 +10,39 @@ import { useLocalAppRegistry, } from './react-context'; +class ActivateHelpersImpl { + private cleanupFns = new Set<() => void>(); + + on = ( + emitter: { + on(evt: string, fn: (...args: any) => any): any; + removeListener(evt: string, fn: (...args: any) => any): any; + }, + evt: string, + fn: (...args: any) => any + ) => { + emitter.on(evt, fn); + this.addCleanup(() => { + emitter.removeListener(evt, fn); + }); + }; + + addCleanup = (fn: () => void) => { + this.cleanupFns.add(fn); + }; + + cleanup = () => { + for (const fn of this.cleanupFns.values()) { + fn(); + } + }; +} + +export type ActivateHelpers = Pick< + ActivateHelpersImpl, + 'on' | 'addCleanup' | 'cleanup' +>; + function LegacyRefluxProvider({ store, actions, @@ -64,7 +97,8 @@ export type HadronPluginConfig unknown>> = { */ activate: ( initialProps: T, - services: Registries & Services + services: Registries & Services, + helpers: ActivateHelpers ) => { /** * Redux or reflux store that will be automatically passed to a @@ -194,11 +228,15 @@ export function registerHadronPlugin< () => localAppRegistry.getPlugin(registryName) ?? (() => { - const plugin = config.activate(props, { - globalAppRegistry, - localAppRegistry, - ...serviceImpls, - }); + const plugin = config.activate( + props, + { + globalAppRegistry, + localAppRegistry, + ...serviceImpls, + }, + new ActivateHelpersImpl() + ); localAppRegistry.registerPlugin(registryName, plugin); return plugin; })() diff --git a/packages/hadron-build/package.json b/packages/hadron-build/package.json index 99b2b6326ac..30c9acacfc8 100644 --- a/packages/hadron-build/package.json +++ b/packages/hadron-build/package.json @@ -33,10 +33,10 @@ "debug": "^4.2.0", "del": "^2.0.2", "download": "^8.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "electron-packager": "^15.5.1", "electron-packager-plugin-non-proprietary-codecs-ffmpeg": "^1.0.2", - "@electron/rebuild": "^3.3.1", + "@electron/rebuild": "^3.4.0", "flatnest": "^1.0.0", "fs-extra": "^8.1.0", "getos": "^3.1.4", diff --git a/packages/hadron-ipc/package.json b/packages/hadron-ipc/package.json index 948edb594c2..781537a953c 100644 --- a/packages/hadron-ipc/package.json +++ b/packages/hadron-ipc/package.json @@ -69,7 +69,7 @@ }, "dependencies": { "debug": "^4.3.4", - "electron": "^25.9.6", + "electron": "^25.9.7", "is-electron-renderer": "^2.0.1" } } diff --git a/scripts/generate-readme-packages-overview.js b/scripts/generate-readme-packages-overview.js index dbe1db37410..1742e9b6498 100644 --- a/scripts/generate-readme-packages-overview.js +++ b/scripts/generate-readme-packages-overview.js @@ -17,13 +17,11 @@ const pluginNames = [ '@mongodb-js/compass-export-to-language', '@mongodb-js/compass-collection', '@mongodb-js/compass-crud', - '@mongodb-js/compass-database', '@mongodb-js/compass-databases-collections', '@mongodb-js/compass-field-store', '@mongodb-js/compass-find-in-page', '@mongodb-js/compass-home', '@mongodb-js/compass-import-export', - '@mongodb-js/compass-instance', '@mongodb-js/compass-query-bar', '@mongodb-js/compass-schema', '@mongodb-js/compass-schema-validation', diff --git a/scripts/package.json b/scripts/package.json index 264ba9c51ca..bcf090ee2c7 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -42,7 +42,7 @@ "@mongodb-js/monorepo-tools": "^1.1.1", "@mongodb-js/webpack-config-compass": "^1.2.5", "commander": "^11.0.0", - "electron": "^25.9.6", + "electron": "^25.9.7", "glob": "^10.2.5", "jsdom": "^21.1.0", "keytar": "^7.9.0",