diff --git a/.eslintignore b/.eslintignore index c188701b9..78e9b517e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -145,4 +145,5 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx __mocks__/monaco-editor.ts src/Explorer/Tree/ResourceTree.tsx +src/Utils/EndpointUtils.ts src/Utils/PriorityBasedExecutionUtils.ts \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a53b92645..fd097a194 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: pull_request: branches: - master +permissions: + id-token: write + contents: read jobs: codemetrics: runs-on: ubuntu-latest @@ -134,7 +137,7 @@ jobs: runs-on: ubuntu-latest env: NODE_TLS_REJECT_UNAUTHORIZED: 0 - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} strategy: fail-fast: false matrix: @@ -145,11 +148,18 @@ jobs: - ./test/mongo/container.spec.ts - ./test/mongo/container32.spec.ts - ./test/selfServe/selfServeExample.spec.ts - # - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/sql/resourceToken.spec.ts - ./test/tables/container.spec.ts steps: - uses: actions/checkout@v4 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v4 with: diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 229477f0b..6698951ae 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -9,6 +9,10 @@ on: # Once every hour - cron: "0 15 * * *" +permissions: + id-token: write + contents: read + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -16,10 +20,17 @@ jobs: name: "Cleanup Test Database Accounts" runs-on: ubuntu-latest env: - NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} steps: - uses: actions/checkout@v2 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: diff --git a/.vscode/settings.json b/.vscode/settings.json index d66e1654c..a57d40961 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,8 +20,8 @@ "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": true + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" }, "typescript.preferences.importModuleSpecifier": "non-relative", "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/configs/mpac.json b/configs/mpac.json index cd8b27e03..0a8e7eaba 100644 --- a/configs/mpac.json +++ b/configs/mpac.json @@ -1,5 +1,5 @@ { - "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", - "isTerminalEnabled" : true, - "isPhoenixEnabled" : true -} \ No newline at end of file + "JUNO_ENDPOINT": "https://tools.cosmos.azure.com", + "isTerminalEnabled": true, + "isPhoenixEnabled": true +} diff --git a/images/Home_16.svg b/images/Home_16.svg new file mode 100644 index 000000000..80facc866 --- /dev/null +++ b/images/Home_16.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index c00efdac6..b4f660063 100644 --- a/jest.config.js +++ b/jest.config.js @@ -76,6 +76,10 @@ module.exports = { "^dnd-core$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", + "d3-force": "/node_modules/d3-force/dist/d3-force.min.js", + "d3-quadtree": "/node_modules/d3-quadtree/dist/d3-quadtree.min.js", + "d3-scale-chromatic": "/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js", + "d3-zoom": "/node_modules/d3-zoom/dist/d3-zoom.min.js", }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader @@ -130,7 +134,6 @@ module.exports = { // The test environment that will be used for testing // testEnvironment: "jest-environment-jsdom", - modulePaths: ["node_modules", "/src"], // Options that will be passed to the testEnvironment diff --git a/less/Common/Constants.less b/less/Common/Constants.less index ea7cf9f9f..5b80d67f8 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -163,6 +163,7 @@ /**********************************************************************************/ @FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; +@FabricToolbarIconColor: "brightness(0) saturate(100%) invert(50%) sepia(17%) saturate(1459%) hue-rotate(81deg) brightness(99%) contrast(94%)"; @FabricBoxBorderRadius: 8px; @FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; @@ -334,4 +335,11 @@ width: 0; height: 0; border-color: @InfoPointerColor transparent; +} +/********************************************************************************************************* + Screen Reader Only +**********************************************************************************************************/ +.screenReaderOnly { + position: absolute; + left: -9999px; } \ No newline at end of file diff --git a/less/documentDB.less b/less/documentDB.less index d8ae159db..357d159b3 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2296,6 +2296,17 @@ a:link { display: none !important; } +.monaco-editor .quick-input-list-label { + /* Restore some of Monaco's default styles that are clobbered by our global styles */ + padding: 0; + line-height: 22px; +} + +.monaco-editor .quick-input-list .highlight { + /* Padding in highlighted text within the quick input list breaks the flow of the text */ + padding: 0; +} + td a { color: #393939; } diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less index 57ad43316..be4e5c17e 100644 --- a/less/documentDBFabric.less +++ b/less/documentDBFabric.less @@ -25,29 +25,30 @@ a:focus { } .resourceTreeAndTabs { - border-radius: @FabricBoxBorderRadius; + border-radius: 0px; box-shadow: @FabricBoxBorderShadow; margin: @FabricBoxMargin; - margin-top: 4px; + margin-top: 0px; + margin-bottom: 0px; background-color: #ffffff; } .tabsManagerContainer { - background-color: #fafafa + background-color: #ffffff } .nav-tabs-margin { padding-top: 8px; - background-color: #fafafa + background-color: #ffffff } .commandBarContainer { background-color: #ffffff; - border-bottom: none; - border-radius: @FabricBoxBorderRadius; + border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px; box-shadow: @FabricBoxBorderShadow; margin: @FabricBoxMargin; margin-top: 0px; + margin-bottom: 0px; padding-top: 2px; padding: 0px; height: 40px; @@ -162,9 +163,10 @@ a:focus { .dataExplorerErrorConsoleContainer { - border-radius: @FabricBoxBorderRadius; + border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius; box-shadow: @FabricBoxBorderShadow; margin: @FabricBoxMargin; + margin-top: 0px; width: auto; align-self: auto; } diff --git a/package-lock.json b/package-lock.json index fd4ba95b8..26c513d6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.0", + "@azure/cosmos": "4.0.1-beta.3", "@azure/cosmos-language-service": "0.0.5", - "@azure/identity": "1.2.1", - "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/identity": "1.5.2", + "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", @@ -51,6 +51,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "file:./canvas", @@ -59,7 +60,7 @@ "copy-webpack-plugin": "11.0.0", "crossroads": "0.12.2", "css-element-queries": "1.1.1", - "d3": "6.1.1", + "d3": "7.8.5", "datatables.net-colreorder-dt": "1.7.0", "datatables.net-dt": "1.13.8", "date-fns": "1.29.0", @@ -74,12 +75,14 @@ "i18next-browser-languagedetector": "6.0.1", "i18next-http-backend": "1.0.23", "iframe-resizer-react": "1.1.0", + "immer": "9.0.6", "immutable": "4.0.0-rc.12", "is-ci": "2.0.0", "jquery": "3.7.1", "jquery-typeahead": "2.11.1", "jquery-ui-dist": "1.13.2", "knockout": "3.5.1", + "loader-utils": "2.0.3", "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", @@ -103,10 +106,12 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", + "shell-quote": "1.7.3", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", - "underscore": "1.9.1", + "tinykeys": "2.1.0", + "underscore": "1.12.1", "utility-types": "3.10.0", "zustand": "3.5.0" }, @@ -175,25 +180,25 @@ "less-vars-loader": "1.1.0", "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", "raw-loader": "0.5.1", - "react-dev-utils": "11.0.4", + "react-dev-utils": "12.0.1", "rimraf": "3.0.0", "sinon": "3.2.1", "style-loader": "0.23.0", "ts-loader": "9.2.4", - "typedoc": "0.21.5", + "typedoc": "0.22.15", "typescript": "4.3.5", "url-loader": "4.1.1", "wait-on": "4.0.2", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "4.15.2" } }, "canvas": { @@ -248,14 +253,6 @@ "tslib": "^1.10.0" } }, - "node_modules/@azure/core-asynciterator-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz", - "integrity": "sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@azure/core-auth": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", @@ -274,66 +271,35 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/@azure/core-http": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", - "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.1", - "form-data": "^3.0.0", - "node-fetch": "^2.6.0", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.2.0", - "tunnel": "^0.0.6", - "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-http/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dependencies": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-http/node_modules/@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@azure/core-http/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@azure/core-http/node_modules/tslib": { + "node_modules/@azure/core-client/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" @@ -396,9 +362,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/cosmos": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.0.tgz", - "integrity": "sha512-/Z27p1+FTkmjmm8jk90zi/HrczPHw2t8WecFnsnTe4xGocWl0Z4clP0YlLUTJPhRLWYa5upwD9rMvKJkS1f1kg==", + "version": "4.0.1-beta.3", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.3.tgz", + "integrity": "sha512-CpRGt+S5jnvtGUi4TmlS79YvxpbNc8/5/QHgIvvQ9D2ZFUqO0MjbMCU3lVZV2NAJT02BsbLfRAFe+FPn8nMRQw==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", @@ -408,14 +374,14 @@ "fast-json-stable-stringify": "^2.1.0", "jsbi": "^3.1.3", "node-abort-controller": "^3.0.0", - "priorityqueuejs": "^1.0.0", + "priorityqueuejs": "^2.0.0", "semaphore": "^1.0.5", "tslib": "^2.2.0", "universal-user-agent": "^6.0.0", "uuid": "^8.3.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/cosmos-language-service": { @@ -454,42 +420,44 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/identity": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.2.1.tgz", - "integrity": "sha512-vCzV4Xg5hWJ2e4Et0waOmIEgYHsqtGF06kklnqblZg0hKDLKxTAX5FzKYuDMk1CctY2UdEmWFcA2li2uOXOLXQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.2.tgz", + "integrity": "sha512-vqyeRbd2i0h9F4mqW5JbkP1xfabqKQ21l/81osKhpOQ2LtwaJW6nw4+0PsVYnxcbPHFCIZt6EWAk74a3OGYZJA==", "dependencies": { - "@azure/core-http": "^1.2.0", - "@azure/core-tracing": "1.0.0-preview.9", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "1.0.0-preview.12", "@azure/logger": "^1.0.0", - "@azure/msal-node": "1.0.0-beta.1", - "@opentelemetry/api": "^0.10.2", + "@azure/msal-node": "1.0.0-beta.6", + "@types/stoppable": "^1.1.0", "axios": "^0.21.1", "events": "^3.0.0", "jws": "^4.0.0", "msal": "^1.0.2", "open": "^7.0.0", "qs": "^6.7.0", + "stoppable": "^1.1.0", "tslib": "^2.0.0", "uuid": "^8.3.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" }, "optionalDependencies": { - "keytar": "^5.4.0" + "keytar": "^7.3.0" } }, "node_modules/@azure/identity/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.9", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", - "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", + "version": "1.0.0-preview.12", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.12.tgz", + "integrity": "sha512-nvo2Wc4EKZGN6eFu9n3U7OXmASmL8VxoPIH7xaD6OlQqi44bouF0YIi9ID5rEsKLiAU59IYx6M297nqWVMWPDg==", "dependencies": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.10.2", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" } }, "node_modules/@azure/identity/node_modules/tslib": { @@ -596,13 +564,13 @@ } }, "node_modules/@azure/ms-rest-nodeauth": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.7.tgz", - "integrity": "sha512-7Q1MyMB+eqUQy8JO+virSIzAjqR2UbKXE/YQZe+53gC8yakm8WOQ5OzGfPP+eyHqeRs6bQESyw2IC5feLWlT2A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.1.1.tgz", + "integrity": "sha512-UA/8dgLy3+ZiwJjAZHxL4MUB14fFQPkaAOZ94jsTW/Z6WmoOeny2+cLk0+dyIX/iH6qSrEWKwbStEeB970B9pA==", "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", - "adal-node": "^0.1.28" + "adal-node": "^0.2.2" } }, "node_modules/@azure/msal-browser": { @@ -629,61 +597,17 @@ } }, "node_modules/@azure/msal-node": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.1.tgz", - "integrity": "sha512-dO/bgVScpl5loZfsfhHXmFLTNoDxGvUiZIsJCe1+HpHyFWXwGsBZ71P5ixbxRhhf/bPpZS3X+/rm1Fq2uUucJw==", + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz", + "integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==", "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", "dependencies": { - "@azure/msal-common": "^1.7.2", - "axios": "^0.19.2", + "@azure/msal-common": "^4.0.0", + "axios": "^0.21.1", "jsonwebtoken": "^8.5.1", "uuid": "^8.3.0" } }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.7.2.tgz", - "integrity": "sha512-3/voCdFKONENX+5tMrNOBSrVJb6NbE7YB8vc4FZ/4ZbjpK7GVtq9Bu1MW+HZhrmsUzSF/joHx0ZIJDYIequ/jg==", - "dependencies": { - "debug": "^4.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node/node_modules/axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", - "dependencies": { - "follow-redirects": "1.5.10" - } - }, - "node_modules/@azure/msal-node/node_modules/follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "dependencies": { - "debug": "=3.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@azure/msal-node/node_modules/follow-redirects/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/@azure/msal-node/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -7066,17 +6990,6 @@ "node": ">=10" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10179,29 +10092,10 @@ "@octokit/openapi-types": "^19.0.2" } }, - "node_modules/@opencensus/web-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", - "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==", - "engines": { - "node": ">=6.0" - } - }, "node_modules/@opentelemetry/api": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", - "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", - "dependencies": { - "@opentelemetry/context-base": "^0.10.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-base": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", - "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", "engines": { "node": ">=8.0.0" } @@ -12834,6 +12728,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "devOptional": true }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, "node_modules/@types/post-robot": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@types/post-robot/-/post-robot-10.0.1.tgz", @@ -13613,6 +13513,14 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "devOptional": true }, + "node_modules/@types/stoppable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.3.tgz", + "integrity": "sha512-7wGKIBJGE4ZxFjk9NkjAxZMLlIXroETqP1FJCdoSvKmEznwmBxQFmTB1dsCkAvVcNemuSZM5qkkd9HE/NL2JTw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/styled-components": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.1.tgz", @@ -13639,14 +13547,6 @@ "@types/jest": "*" } }, - "node_modules/@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/underscore": { "version": "1.7.36", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.7.36.tgz", @@ -14183,6 +14083,14 @@ } } }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -14300,29 +14208,39 @@ } }, "node_modules/adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha512-98nQ5MQSyJR0ZY/R0Mue/cv4OkebRyKz4hS40GdkZU42Bq49ldHeup7UeAo/0vROMB57CX2et6IF0U/Pe1rY3A==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.4.tgz", + "integrity": "sha512-zIcvbwQFKMUtKxxj8YMHeTT1o/TPXfVNsTXVgXD8sxwV6h4AFQgK77dRciGhuEF9/Sdm3UQPJVPc/6XxrccSeA==", "deprecated": "This package is no longer supported. Please migrate to @azure/msal-node.", "dependencies": { - "@types/node": "^8.0.47", - "async": ">=0.6.0", + "@xmldom/xmldom": "^0.8.3", + "async": "^2.6.3", + "axios": "^0.21.1", "date-utils": "*", "jws": "3.x.x", - "request": ">= 2.52.0", "underscore": ">= 1.3.1", "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", "xpath.js": "~1.1.0" }, "engines": { "node": ">= 0.6.15" } }, - "node_modules/adal-node/node_modules/@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + "node_modules/adal-node/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/adal-node/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } }, "node_modules/adal-node/node_modules/jwa": { "version": "1.4.1", @@ -14638,7 +14556,9 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/archy": { "version": "1.0.0", @@ -14646,16 +14566,6 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -14939,7 +14849,9 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "peer": true }, "node_modules/async-hook-jl": { "version": "1.7.6", @@ -15083,6 +14995,32 @@ "webpack": ">=2" } }, + "node_modules/babel-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/babel-loader/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -16418,7 +16356,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16695,7 +16635,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -16969,7 +16911,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "devOptional": true + "dev": true }, "node_modules/cosmiconfig": { "version": "5.2.1", @@ -17317,40 +17259,43 @@ } }, "node_modules/d3": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/d3/-/d3-6.1.1.tgz", - "integrity": "sha512-bJYW9wlS2uvP2EoMkcPptrUzLMHQKCbiSW+/la8iGSLZgs4KbI/f3Fch4RtnUA9PA+/nPlwyFYzTwDjX80Of8w==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", "dependencies": { - "d3-array": "2", - "d3-axis": "2", - "d3-brush": "2", - "d3-chord": "2", - "d3-color": "2", - "d3-contour": "2", - "d3-delaunay": "5", - "d3-dispatch": "2", - "d3-drag": "2", - "d3-dsv": "2", - "d3-ease": "2", - "d3-fetch": "2", - "d3-force": "2", - "d3-format": "2", - "d3-geo": "2", - "d3-hierarchy": "2", - "d3-interpolate": "2", - "d3-path": "2", - "d3-polygon": "2", - "d3-quadtree": "2", - "d3-random": "2", - "d3-scale": "3", - "d3-scale-chromatic": "2", - "d3-selection": "2", - "d3-shape": "2", - "d3-time": "2", - "d3-time-format": "3", - "d3-timer": "2", - "d3-transition": "2", - "d3-zoom": "2" + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-array": { @@ -17362,9 +17307,12 @@ } }, "node_modules/d3-axis": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz", - "integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-brush": { "version": "2.1.0", @@ -17379,11 +17327,14 @@ } }, "node_modules/d3-chord": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz", - "integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "dependencies": { - "d3-path": "1 - 2" + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-collection": { @@ -17397,19 +17348,36 @@ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" }, "node_modules/d3-contour": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz", - "integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "dependencies": { - "d3-array": "2" + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-delaunay": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz", - "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dependencies": { - "delaunator": "4" + "delaunator": "5" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-dispatch": { @@ -17427,24 +17395,46 @@ } }, "node_modules/d3-dsv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", - "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "dependencies": { - "commander": "2", - "iconv-lite": "0.4", + "commander": "7", + "iconv-lite": "0.6", "rw": "1" }, "bin": { - "csv2json": "bin/dsv2json", - "csv2tsv": "bin/dsv2dsv", - "dsv2dsv": "bin/dsv2dsv", - "dsv2json": "bin/dsv2json", - "json2csv": "bin/json2dsv", - "json2dsv": "bin/json2dsv", - "json2tsv": "bin/json2dsv", - "tsv2csv": "bin/dsv2dsv", - "tsv2json": "bin/dsv2json" + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/d3-ease": { @@ -17453,21 +17443,27 @@ "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==" }, "node_modules/d3-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz", - "integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "dependencies": { - "d3-dsv": "1 - 2" + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-force": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", - "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "dependencies": { - "d3-dispatch": "1 - 2", - "d3-quadtree": "1 - 2", - "d3-timer": "1 - 2" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-format": { @@ -17476,11 +17472,14 @@ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" }, "node_modules/d3-geo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", - "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "dependencies": { - "d3-array": "^2.5.0" + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-glyphedge": { @@ -17494,9 +17493,12 @@ "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==" }, "node_modules/d3-hierarchy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", - "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-interpolate": { "version": "2.0.1", @@ -17507,9 +17509,12 @@ } }, "node_modules/d3-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", - "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-path-arrows": { "version": "0.4.0", @@ -17531,19 +17536,28 @@ "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" }, "node_modules/d3-polygon": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", - "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-quadtree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", - "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-random": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz", - "integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-sankey-circular": { "version": "0.25.0", @@ -17573,12 +17587,15 @@ } }, "node_modules/d3-scale-chromatic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", - "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "dependencies": { - "d3-color": "1 - 2", - "d3-interpolate": "1 - 2" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-selection": { @@ -17641,23 +17658,181 @@ "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" }, "node_modules/d3-zoom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", - "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" } }, "node_modules/d3/node_modules/d3-shape": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", - "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { - "d3-path": "1 - 2" + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, "node_modules/dashdash": { @@ -17810,7 +17985,9 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -18077,9 +18254,12 @@ } }, "node_modules/delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -18099,7 +18279,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/denodeify": { "version": "1.2.1", @@ -18153,15 +18335,12 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -20274,9 +20453,9 @@ "optional": true }, "node_modules/filesize": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", "dev": true, "engines": { "node": ">= 0.4.0" @@ -20674,22 +20853,200 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", - "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.5.5", - "chalk": "^2.4.1", - "micromatch": "^3.1.10", + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", "minimatch": "^3.0.4", - "semver": "^5.6.0", - "tapable": "^1.0.0", - "worker-rpc": "^0.1.0" + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" }, "engines": { - "node": ">=6.11.5", + "node": ">=10", "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { @@ -20701,6 +21058,30 @@ "node": ">=6" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -20936,43 +21317,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -21324,16 +21668,18 @@ "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" }, "node_modules/gzip-size": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", - "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "dependencies": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" + "duplexer": "^0.1.2" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/handle-thing": { @@ -21342,36 +21688,6 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -21469,7 +21785,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/has-value": { "version": "1.0.0", @@ -21758,6 +22076,32 @@ "html-loader": "^0.5.1" } }, + "node_modules/html-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/html-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/html-minifier": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", @@ -22237,6 +22581,14 @@ "node-fetch": "2.6.1" } }, + "node_modules/i18next-http-backend/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -22332,10 +22684,9 @@ } }, "node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", - "dev": true, + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -22787,7 +23138,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -28790,9 +29143,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -28919,22 +29272,16 @@ "integrity": "sha512-yQa1dz+FilQ+w3JM6GH2V/wnFeQhfbkK9stvs3UiraW3GOEO7zrOBBh0ZuHsrzeN1xx6v7P5EpA2JtOUUnfN/w==" }, "node_modules/keytar": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-5.6.0.tgz", - "integrity": "sha512-ueulhshHSGoryfRXaIvTj0BV1yB0KddBGhGoqCxSN9LR1Ks1GKuuCdVhF+2/YOs5fMl6MlTI9On1a4DHDXoTow==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "hasInstallScript": true, "optional": true, "dependencies": { - "nan": "2.14.1", - "prebuild-install": "5.3.3" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, - "node_modules/keytar/node_modules/nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "optional": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -29208,29 +29555,16 @@ } }, "node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "json5": "^2.1.2" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" + "node": ">=8.9.0" } }, "node_modules/locate-path": { @@ -29612,15 +29946,15 @@ } }, "node_modules/marked": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", - "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/martinez-polygon-clipping": { @@ -31326,12 +31660,6 @@ "node": ">=12" } }, - "node_modules/microevent.ts": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", - "dev": true - }, "node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -31407,7 +31735,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -31822,19 +32152,61 @@ } }, "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.60.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.60.0.tgz", + "integrity": "sha512-zcGgwoXbzw9NczqbGzAWL/ToDYAxv1V8gL1D67ClbdkIfeeDBbY0GelZtC25ayLvVjr2q2cloHeQV1R0QAWqRQ==", "optional": true, "dependencies": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" } }, + "node_modules/node-abi/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "optional": true + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -31849,11 +32221,22 @@ } }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-forge": { @@ -31952,12 +32335,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", - "optional": true - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -32011,18 +32388,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -32045,7 +32410,9 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -33519,44 +33886,81 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prebuild-install": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", - "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" + "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/prebuild-install/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/prebuild-install/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "optional": true, "dependencies": { - "minimist": "^1.2.6" + "mimic-response": "^3.1.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, "node_modules/prelude-ls": { @@ -33707,9 +34111,9 @@ } }, "node_modules/priorityqueuejs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", - "integrity": "sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz", + "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==" }, "node_modules/prismjs": { "version": "1.29.0", @@ -33740,7 +34144,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "devOptional": true + "dev": true }, "node_modules/process-on-spawn": { "version": "1.0.0", @@ -34147,106 +34551,110 @@ } }, "node_modules/react-dev-utils": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", - "integrity": "sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", "dev": true, "dependencies": { - "@babel/code-frame": "7.10.4", - "address": "1.1.2", - "browserslist": "4.14.2", - "chalk": "2.4.2", - "cross-spawn": "7.0.3", - "detect-port-alt": "1.1.6", - "escape-string-regexp": "2.0.0", - "filesize": "6.1.0", - "find-up": "4.1.0", - "fork-ts-checker-webpack-plugin": "4.1.6", - "global-modules": "2.0.0", - "globby": "11.0.1", - "gzip-size": "5.1.1", - "immer": "8.0.1", - "is-root": "2.1.0", - "loader-utils": "2.0.0", - "open": "^7.0.2", - "pkg-up": "3.1.0", - "prompts": "2.4.0", - "react-error-overlay": "^6.0.9", - "recursive-readdir": "2.2.2", - "shell-quote": "1.7.2", - "strip-ansi": "6.0.0", - "text-table": "0.2.0" + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/react-dev-utils/node_modules/browserslist": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", - "integrity": "sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001125", - "electron-to-chromium": "^1.3.564", - "escalade": "^3.0.2", - "node-releases": "^1.1.61" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" }, "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/react-dev-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - }, "engines": { "node": ">=10" }, @@ -34254,48 +34662,94 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, "engines": { - "node": ">=8.9.0" + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" } }, "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-dev-utils/node_modules/node-releases": { - "version": "1.1.77", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/react-dev-utils/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/path-exists": { @@ -34307,35 +34761,13 @@ "node": ">=8" } }, - "node_modules/react-dev-utils/node_modules/prompts": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", - "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/react-dev-utils/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" @@ -34877,7 +35309,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "devOptional": true, + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -34892,7 +35324,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "devOptional": true + "dev": true }, "node_modules/readdirp": { "version": "3.6.0", @@ -35674,6 +36106,11 @@ "rimraf": "bin.js" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -36409,10 +36846,9 @@ } }, "node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" }, "node_modules/shellwords": { "version": "0.1.1", @@ -36420,9 +36856,9 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "node_modules/shiki": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.15.tgz", - "integrity": "sha512-/Y0z9IzhJ8nD9nbceORCqu6NgT9X6I8Fk8c3SICHI5NbZRLdZYFaB233gwct9sU0vvSypyaL/qaKvzyQGJBZSw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", + "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", "dev": true, "dependencies": { "jsonc-parser": "^3.0.0", @@ -36482,7 +36918,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -37051,6 +37489,15 @@ "node": ">=0.10.0" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -37083,7 +37530,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -37097,7 +37546,9 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -37106,7 +37557,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -37254,6 +37707,32 @@ "node": ">= 0.12.0" } }, + "node_modules/style-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/style-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/style-loader/node_modules/schema-utils": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", @@ -37786,6 +38265,11 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, + "node_modules/tinykeys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinykeys/-/tinykeys-2.1.0.tgz", + "integrity": "sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q==" + }, "node_modules/tinyqueue": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", @@ -37885,6 +38369,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "devOptional": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -37899,6 +38384,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "devOptional": true, "engines": { "node": ">= 4.0.0" } @@ -38297,19 +38783,16 @@ } }, "node_modules/typedoc": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.5.tgz", - "integrity": "sha512-uRDRmYheE5Iju9Zz0X50pTASTpBorIHFt02F5Y8Dt4eBt55h3mwk1CBSY2+EfwBxY16N4Xm7f8KXhnfFZ0AmBw==", + "version": "0.22.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.15.tgz", + "integrity": "sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q==", "dev": true, "dependencies": { - "glob": "^7.1.7", - "handlebars": "^4.7.7", + "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^2.1.1", - "minimatch": "^3.0.0", - "progress": "^2.0.3", - "shiki": "^0.9.3", - "typedoc-default-themes": "^0.12.10" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" }, "bin": { "typedoc": "bin/typedoc" @@ -38318,16 +38801,28 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x" } }, - "node_modules/typedoc-default-themes": { - "version": "0.12.10", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", - "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">= 8" + "node": ">=10" } }, "node_modules/typescript": { @@ -38437,9 +38932,9 @@ } }, "node_modules/underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "node_modules/unherit": { "version": "1.1.3", @@ -39264,21 +39759,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-bundle-analyzer/node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", @@ -39355,9 +39835,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -39431,9 +39911,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -39464,7 +39944,7 @@ "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", + "webpack-dev-middleware": "^5.3.4", "ws": "^8.13.0" }, "bin": { @@ -39784,15 +40264,6 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -39815,7 +40286,9 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -39948,21 +40421,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/worker-rpc": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", - "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", - "dev": true, - "dependencies": { - "microevent.ts": "~0.1.1" - } - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -40108,26 +40566,6 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", @@ -40143,14 +40581,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "devOptional": true }, - "node_modules/xmldom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", - "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/xpath.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", diff --git a/package.json b/package.json index 3f4252d56..e1d544d1d 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.0", + "@azure/cosmos": "4.0.1-beta.3", "@azure/cosmos-language-service": "0.0.5", - "@azure/identity": "1.2.1", - "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/identity": "1.5.2", + "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", @@ -46,6 +46,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "file:./canvas", @@ -54,7 +55,7 @@ "copy-webpack-plugin": "11.0.0", "crossroads": "0.12.2", "css-element-queries": "1.1.1", - "d3": "6.1.1", + "d3": "7.8.5", "datatables.net-colreorder-dt": "1.7.0", "datatables.net-dt": "1.13.8", "date-fns": "1.29.0", @@ -69,17 +70,19 @@ "i18next-browser-languagedetector": "6.0.1", "i18next-http-backend": "1.0.23", "iframe-resizer-react": "1.1.0", + "immer": "9.0.6", "immutable": "4.0.0-rc.12", "is-ci": "2.0.0", "jquery": "3.7.1", "jquery-typeahead": "2.11.1", "jquery-ui-dist": "1.13.2", "knockout": "3.5.1", + "loader-utils": "2.0.3", "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", - "patch-package": "8.0.0", "p-retry": "4.6.2", + "patch-package": "8.0.0", "plotly.js-cartesian-dist-min": "1.52.3", "post-robot": "10.0.42", "q": "1.5.1", @@ -98,10 +101,12 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", + "shell-quote": "1.7.3", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", - "underscore": "1.9.1", + "tinykeys": "2.1.0", + "underscore": "1.12.1", "utility-types": "3.10.0", "zustand": "3.5.0" }, @@ -170,25 +175,25 @@ "less-vars-loader": "1.1.0", "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", "raw-loader": "0.5.1", - "react-dev-utils": "11.0.4", + "react-dev-utils": "12.0.1", "rimraf": "3.0.0", "sinon": "3.2.1", "style-loader": "0.23.0", "ts-loader": "9.2.4", - "typedoc": "0.21.5", + "typedoc": "0.22.15", "typescript": "4.3.5", "url-loader": "4.1.1", "wait-on": "4.0.2", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "4.15.2" }, "scripts": { "postinstall": "patch-package", @@ -238,4 +243,4 @@ "printWidth": 120, "endOfLine": "auto" } -} \ No newline at end of file +} diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 4d86f5ff7..d56f5087a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -88,6 +88,12 @@ export class CapabilityNames { public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableServerless: string = "EnableServerless"; + public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch"; +} + +export enum CapacityMode { + Provisioned = "Provisioned", + Serverless = "Serverless", } // flight names returned from the portal are always lowercase @@ -124,7 +130,36 @@ export enum MongoBackendEndpointType { remote, } -// TODO: 435619 Add default endpoints per cloud and use regional only when available +export class BackendApi { + public static readonly GenerateToken: string = "GenerateToken"; + public static readonly PortalSettings: string = "PortalSettings"; +} + +export class PortalBackendEndpoints { + public static readonly Development: string = "https://localhost:7235"; + public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn"; +} + +export class MongoProxyEndpoints { + public static readonly Local: string = "https://localhost:7238"; + public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; +} + +export class CassandraProxyEndpoints { + public static readonly Development: string = "https://localhost:7240"; + public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; + public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com"; + public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us"; + public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn"; +} + +//TODO: Remove this when new backend is migrated over export class CassandraBackend { public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete"; @@ -136,6 +171,17 @@ export class CassandraBackend { public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; } +export class CassandraProxyAPIs { + public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; + public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete"; + public static readonly queryApi: string = "api/cassandra"; + public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra"; + public static readonly keysApi: string = "api/cassandra/keys"; + public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys"; + public static readonly schemaApi: string = "api/cassandra/schema"; + public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema"; +} + export class Queries { public static CustomPageOption: string = "custom"; public static UnlimitedPageOption: string = "unlimited"; @@ -211,6 +257,10 @@ export class HttpHeaders { public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; } +export class ContentType { + public static applicationJson: string = "application/json"; +} + export class ApiType { // Mapped to hexadecimal values in the backend public static readonly MongoDB: number = 1; diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index 2a1913c83..c24680f4b 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,7 +1,6 @@ import * as Cosmos from "@azure/cosmos"; -import { sendCachedDataMessage } from "Common/MessageHandler"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; -import { AuthorizationToken, FabricMessageTypes } from "Contracts/FabricMessageTypes"; +import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { AuthType } from "../AuthType"; @@ -51,15 +50,23 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { case Cosmos.ResourceType.offer: case Cosmos.ResourceType.user: case Cosmos.ResourceType.permission: - // User master tokens - const authorizationToken = await sendCachedDataMessage( - FabricMessageTypes.GetAuthorizationToken, - [requestInfo], - userContext.fabricContext.connectionId, - ); - console.log("Response from Fabric: ", authorizationToken); - headers[HttpHeaders.msDate] = authorizationToken.XDate; - return decodeURIComponent(authorizationToken.PrimaryReadWriteToken); + // For now, these operations aren't used, so fetching the authorization token is commented out. + // This provider must return a real token to pass validation by the client, so we return the cached resource token + // (which is a valid token, but won't work for these operations). + const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens; + return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId); + + /* ************** TODO: Uncomment this code if we need to support these operations ************** + // User master tokens + const authorizationToken = await sendCachedDataMessage( + FabricMessageTypes.GetAuthorizationToken, + [requestInfo], + userContext.fabricContext.connectionId, + ); + console.log("Response from Fabric: ", authorizationToken); + headers[HttpHeaders.msDate] = authorizationToken.XDate; + return decodeURIComponent(authorizationToken.PrimaryReadWriteToken); + ***********************************************************************************************/ } } diff --git a/src/Common/IteratorUtilities.ts b/src/Common/IteratorUtilities.ts index f85ad7fb2..6283488b8 100644 --- a/src/Common/IteratorUtilities.ts +++ b/src/Common/IteratorUtilities.ts @@ -1,3 +1,4 @@ +import { QueryOperationOptions } from "@azure/cosmos"; import { QueryResults } from "../Contracts/ViewModels"; interface QueryResponse { @@ -10,13 +11,17 @@ interface QueryResponse { } export interface MinimalQueryIterator { - fetchNext: () => Promise; + fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise; } // Pick, "fetchNext">; -export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise { - return documentsIterator.fetchNext().then((response) => { +export function nextPage( + documentsIterator: MinimalQueryIterator, + firstItemIndex: number, + queryOperationOptions?: QueryOperationOptions, +): Promise { + return documentsIterator.fetchNext(queryOperationOptions).then((response) => { const documents = response.resources; // eslint-disable-next-line @typescript-eslint/no-explicit-any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index ea4103ff6..d9aa0fb4c 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -1,6 +1,10 @@ import { Constants as CosmosSDKConstants } from "@azure/cosmos"; +import { + allowedMongoProxyEndpoints, + allowedMongoProxyEndpoints_ToBeDeprecated, + validateEndpoint, +} from "Utils/EndpointUtils"; import queryString from "querystring"; -import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation"; import { AuthType } from "../AuthType"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; @@ -10,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId"; import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; -import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; +import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants"; import { MinimalQueryIterator } from "./IteratorUtilities"; import { sendMessage } from "./MessageHandler"; @@ -62,6 +66,73 @@ export function queryDocuments( isResourceList: boolean, query: string, continuationToken?: string, +): Promise { + if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) { + return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken); + } + + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`, + resourceID: collection.rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + collection && collection.partitionKey && !collection.partitionKey.systemKey + ? collection.partitionKeyProperties?.[0] + : "", + query, + }; + + const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; + + const headers = { + ...defaultHeaders, + ...authHeaders(), + [CosmosSDKConstants.HttpHeaders.IsQuery]: "true", + [CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true", + [CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true", + [CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true", + [CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true", + [HttpHeaders.contentType]: "application/query+json", + }; + + if (continuationToken) { + headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken; + } + + const path = isResourceList ? "/resourcelist" : "/queryDocuments"; + + return window + .fetch(`${endpoint}${path}`, { + method: "POST", + body: JSON.stringify(params), + headers, + }) + .then(async (response) => { + if (response.ok) { + return { + continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation), + documents: (await response.json()).Documents as DataModels.DocumentId[], + headers: response.headers, + }; + } + await errorHandling(response, "querying documents", params); + return undefined; + }); +} + +function queryDocuments_ToBeDeprecated( + databaseId: string, + collection: Collection, + isResourceList: boolean, + query: string, + continuationToken?: string, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -122,6 +193,54 @@ export function readDocument( databaseId: string, collection: Collection, documentId: DocumentId, +): Promise { + if (!useMongoProxyEndpoint("readDocument")) { + return readDocument_ToBeDeprecated(databaseId, collection, documentId); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const idComponents = documentId.self.split("/"); + const path = idComponents.slice(0, 4).join("/"); + const rid = encodeURIComponent(idComponents[5]); + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}${path}/${rid}`, + resourceID: rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperties?.[0] + : "", + }; + + const endpoint = getFeatureEndpointOrDefault("readDocument"); + + return window + .fetch(endpoint, { + method: "POST", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "reading document", params); + }); +} + +export function readDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + documentId: DocumentId, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -169,6 +288,51 @@ export function createDocument( collection: Collection, partitionKeyProperty: string, documentContent: unknown, +): Promise { + if (!useMongoProxyEndpoint("createDocument")) { + return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`, + resourceID: collection.rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", + documentContent: JSON.stringify(documentContent), + }; + + const endpoint = getFeatureEndpointOrDefault("createDocument"); + + return window + .fetch(`${endpoint}/createDocument`, { + method: "POST", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "creating document", params); + }); +} + +export function createDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + partitionKeyProperty: string, + documentContent: unknown, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -208,6 +372,56 @@ export function updateDocument( collection: Collection, documentId: DocumentId, documentContent: string, +): Promise { + if (!useMongoProxyEndpoint("updateDocument")) { + return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const idComponents = documentId.self.split("/"); + const path = idComponents.slice(0, 5).join("/"); + const rid = encodeURIComponent(idComponents[5]); + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}${path}/${rid}`, + resourceID: rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperties?.[0] + : "", + documentContent, + }; + const endpoint = getFeatureEndpointOrDefault("updateDocument"); + + return window + .fetch(endpoint, { + method: "PUT", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "updating document", params); + }); +} + +export function updateDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + documentId: DocumentId, + documentContent: string, ): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; @@ -237,7 +451,7 @@ export function updateDocument( headers: { ...defaultHeaders, ...authHeaders(), - [HttpHeaders.contentType]: "application/json", + [HttpHeaders.contentType]: ContentType.applicationJson, [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) @@ -250,6 +464,53 @@ export function updateDocument( } export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise { + if (!useMongoProxyEndpoint("deleteDocument")) { + return deleteDocument_ToBeDeprecated(databaseId, collection, documentId); + } + const { databaseAccount } = userContext; + const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; + const idComponents = documentId.self.split("/"); + const path = idComponents.slice(0, 5).join("/"); + const rid = encodeURIComponent(idComponents[5]); + const params = { + databaseID: databaseId, + collectionID: collection.id(), + resourceUrl: `${resourceEndpoint}${path}/${rid}`, + resourceID: rid, + resourceType: "docs", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: + documentId && documentId.partitionKey && !documentId.partitionKey.systemKey + ? documentId.partitionKeyProperties?.[0] + : "", + }; + const endpoint = getFeatureEndpointOrDefault("deleteDocument"); + + return window + .fetch(endpoint, { + method: "DELETE", + body: JSON.stringify(params), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return undefined; + } + return await errorHandling(response, "deleting document", params); + }); +} + +export function deleteDocument_ToBeDeprecated( + databaseId: string, + collection: Collection, + documentId: DocumentId, +): Promise { const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const idComponents = documentId.self.split("/"); @@ -277,7 +538,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum headers: { ...defaultHeaders, ...authHeaders(), - [HttpHeaders.contentType]: "application/json", + [HttpHeaders.contentType]: ContentType.applicationJson, [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) @@ -291,6 +552,52 @@ export function deleteDocument(databaseId: string, collection: Collection, docum export function createMongoCollectionWithProxy( params: DataModels.CreateCollectionParams, +): Promise { + if (!useMongoProxyEndpoint("createCollectionWithProxy")) { + return createMongoCollectionWithProxy_ToBeDeprecated(params); + } + const { databaseAccount } = userContext; + const shardKey: string = params.partitionKey?.paths[0]; + + const createCollectionParams = { + databaseID: params.databaseId, + collectionID: params.collectionId, + resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint, + resourceID: "", + resourceType: "colls", + subscriptionID: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + databaseAccountName: databaseAccount.name, + partitionKey: shardKey, + isAutoscale: !!params.autoPilotMaxThroughput, + hasSharedThroughput: params.databaseLevelThroughput, + offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput, + createDatabase: params.createNewDatabase, + isSharded: !!shardKey, + }; + + const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); + + return window + .fetch(`${endpoint}/createCollection`, { + method: "POST", + body: JSON.stringify(createCollectionParams), + headers: { + ...defaultHeaders, + ...authHeaders(), + [HttpHeaders.contentType]: ContentType.applicationJson, + }, + }) + .then(async (response) => { + if (response.ok) { + return response.json(); + } + return await errorHandling(response, "creating collection", createCollectionParams); + }); +} + +export function createMongoCollectionWithProxy_ToBeDeprecated( + params: DataModels.CreateCollectionParams, ): Promise { const { databaseAccount } = userContext; const shardKey: string = params.partitionKey?.paths[0]; @@ -334,13 +641,20 @@ export function createMongoCollectionWithProxy( return await errorHandling(response, "creating collection", mongoParams); }); } - export function getFeatureEndpointOrDefault(feature: string): string { - const endpoint = - hasFlag(userContext.features.mongoProxyAPIs, feature) && - validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints) - ? userContext.features.mongoProxyEndpoint - : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; + let endpoint; + if (useMongoProxyEndpoint(feature)) { + endpoint = configContext.MONGO_PROXY_ENDPOINT; + } else { + endpoint = + hasFlag(userContext.features.mongoProxyAPIs, feature) && + validateEndpoint(userContext.features.mongoProxyEndpoint, [ + ...allowedMongoProxyEndpoints, + ...allowedMongoProxyEndpoints_ToBeDeprecated, + ]) + ? userContext.features.mongoProxyEndpoint + : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; + } return getEndpoint(endpoint); } @@ -349,11 +663,37 @@ export function getEndpoint(endpoint: string): string { let url = endpoint + "/api/mongo/explorer"; if (userContext.authType === AuthType.EncryptedToken) { - url = url.replace("api/mongo", "api/guest/mongo"); + if (endpoint === configContext.MONGO_PROXY_ENDPOINT) { + url = url.replace("api/mongo", "api/connectionstring/mongo"); + } else { + url = url.replace("api/mongo", "api/guest/mongo"); + } } return url; } +export function useMongoProxyEndpoint(api: string): boolean { + const activeMongoProxyEndpoints: string[] = [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + ]; + let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if ( + configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { + canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + + return ( + canAccessMongoProxy && + configContext.NEW_MONGO_APIS?.includes(api) && + activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) + ); +} + // TODO: This function throws most of the time except on Forbidden which is a bit strange // It causes problems for TypeScript understanding the types async function errorHandling(response: Response, action: string, params: unknown): Promise { diff --git a/src/Common/TableEntity.tsx b/src/Common/TableEntity.tsx index f3d0b5244..eece32ecb 100644 --- a/src/Common/TableEntity.tsx +++ b/src/Common/TableEntity.tsx @@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent = ({ editEntity = ({ delete entity => { + let dataTransferJobs: DataTransferJobGetResults[] = []; + let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount( + subscriptionId, + resourceGroup, + accountName, + ); + dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; + while (dataTransferFeeds?.nextLink) { + const nextResponse = await window.fetch(dataTransferFeeds.nextLink, { + headers: { + Authorization: userContext.authorizationToken, + }, + }); + if (nextResponse.ok) { + dataTransferFeeds = await nextResponse.json(); + dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; + } else { + break; + } + } + return dataTransferJobs; +}; + +export const initiateDataTransfer = async (params: DataTransferParams): Promise => { + const { + jobName, + apiType, + subscriptionId, + resourceGroupName, + accountName, + sourceDatabaseName, + sourceCollectionName, + targetDatabaseName, + targetCollectionName, + } = params; + const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName); + const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName); + const body: CreateJobRequest = { + properties: { + source: sourcePayload, + destination: targetPayload, + }, + }; + return create(subscriptionId, resourceGroupName, accountName, jobName, body); +}; + +export const pollDataTransferJob = async ( + jobName: string, + subscriptionId: string, + resourceGroupName: string, + accountName: string, +): Promise => { + const currentPollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs; + if (currentPollingJobs.has(jobName)) { + return; + } + let clearMessage = NotificationConsoleUtils.logConsoleProgress(`Data transfer job ${jobName} in progress`); + return await promiseRetry( + () => pollDataTransferJobOperation(jobName, subscriptionId, resourceGroupName, accountName, clearMessage), + { + retries: 500, + maxTimeout: 5000, + onFailedAttempt: (error: FailedAttemptError) => { + clearMessage(); + clearMessage = NotificationConsoleUtils.logConsoleProgress(error.message); + }, + }, + ); +}; + +const pollDataTransferJobOperation = async ( + jobName: string, + subscriptionId: string, + resourceGroupName: string, + accountName: string, + clearMessage?: () => void, +): Promise => { + if (!userContext.authorizationToken) { + throw new Error("No authority token provided"); + } + + addToPolling(jobName); + + const body: DataTransferJobGetResults = await get(subscriptionId, resourceGroupName, accountName, jobName); + const status = body?.properties?.status; + + updateDataTransferJob(body); + + if (status === "Cancelled") { + removeFromPolling(jobName); + clearMessage && clearMessage(); + const cancelMessage = `Data transfer job ${jobName} cancelled`; + NotificationConsoleUtils.logConsoleError(cancelMessage); + throw new AbortError(cancelMessage); + } + if (status === "Failed" || status === "Faulted") { + removeFromPolling(jobName); + const errorMessage = body?.properties?.error + ? JSON.stringify(body?.properties?.error) + : "Operation could not be completed"; + const error = new Error(errorMessage); + clearMessage && clearMessage(); + NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`); + throw new AbortError(error); + } + if (status === "Completed") { + removeFromPolling(jobName); + clearMessage && clearMessage(); + NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`); + return body; + } + const processedCount = body.properties.processedCount; + const totalCount = body.properties.totalCount; + const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`; + throw new Error(retryMessage); +}; + +export const cancelDataTransferJob = async ( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise => { + const cancelResult: DataTransferJobGetResults = await cancel(subscriptionId, resourceGroupName, accountName, jobName); + updateDataTransferJob(cancelResult); + removeFromPolling(cancelResult?.properties?.jobName); +}; + +const createPayload = ( + apiType: ApiType, + databaseName: string, + containerName: string, +): + | CosmosSqlDataTransferDataSourceSink + | CosmosMongoDataTransferDataSourceSink + | CosmosCassandraDataTransferDataSourceSink => { + switch (apiType) { + case "SQL": + return { + component: "CosmosDBSql", + databaseName: databaseName, + containerName: containerName, + } as CosmosSqlDataTransferDataSourceSink; + case "Mongo": + return { + component: "CosmosDBMongo", + databaseName: databaseName, + collectionName: containerName, + } as CosmosMongoDataTransferDataSourceSink; + case "Cassandra": + return { + component: "CosmosDBCassandra", + keyspaceName: databaseName, + tableName: containerName, + }; + default: + throw new Error(`Unsupported API type for data transfer: ${apiType}`); + } +}; diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 0b8ebd29d..223fe987d 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -1,4 +1,5 @@ import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Queries } from "../Constants"; import { client } from "../CosmosClient"; @@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || Queries.itemsPerPage; options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); + options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled(); return options; }; diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 556ed290c..17e84ba28 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -1,3 +1,4 @@ +import { QueryOperationOptions } from "@azure/cosmos"; import { QueryResults } from "../../Contracts/ViewModels"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { getEntityName } from "../DocumentUtility"; @@ -8,12 +9,13 @@ export const queryDocumentsPage = async ( resourceName: string, documentsIterator: MinimalQueryIterator, firstItemIndex: number, + queryOperationOptions?: QueryOperationOptions, ): Promise => { const entityName = getEntityName(); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); try { - const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); + const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions); const itemCount = (result.documents && result.documents.length) || 0; logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); return result; diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 1679a3657..ed8eaab0e 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,17 +1,25 @@ -import { JunoEndpoints } from "Common/Constants"; +import { + BackendApi, + CassandraProxyEndpoints, + JunoEndpoints, + MongoProxyEndpoints, + PortalBackendEndpoints, +} from "Common/Constants"; import { allowedAadEndpoints, allowedArcadiaEndpoints, + allowedCassandraProxyEndpoints, allowedEmulatorEndpoints, allowedGraphEndpoints, allowedHostedExplorerEndpoints, allowedJunoOrigins, allowedMongoBackendEndpoints, + allowedMongoProxyEndpoints, allowedMsalRedirectEndpoints, defaultAllowedArmEndpoints, defaultAllowedBackendEndpoints, validateEndpoint, -} from "Utils/EndpointValidation"; +} from "Utils/EndpointUtils"; export enum Platform { Portal = "Portal", @@ -37,7 +45,15 @@ export interface ConfigContext { ARCADIA_ENDPOINT: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; BACKEND_ENDPOINT?: string; + PORTAL_BACKEND_ENDPOINT?: string; + NEW_BACKEND_APIS?: BackendApi[]; MONGO_BACKEND_ENDPOINT?: string; + MONGO_PROXY_ENDPOINT?: string; + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean; + NEW_MONGO_APIS?: string[]; + CASSANDRA_PROXY_ENDPOINT?: string; + CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean; + NEW_CASSANDRA_APIS?: string[]; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; @@ -67,6 +83,7 @@ let configContext: Readonly = { `^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`, + `^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`, ], // Webpack injects this at build time gitSha: process.env.GIT_SHA, hostedExplorerURL: "https://cosmos.azure.com/", @@ -82,6 +99,22 @@ let configContext: Readonly = { GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: JunoEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, + NEW_MONGO_APIS: [ + // "resourcelist", + // "queryDocuments", + // "createDocument", + // "readDocument", + // "updateDocument", + // "deleteDocument", + // "createCollectionWithProxy", + "legacyMongoShell", + ], + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, + CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, + NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], + CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, isTerminalEnabled: false, isPhoenixEnabled: false, }; @@ -127,10 +160,18 @@ export function updateConfigContext(newContext: Partial): void { delete newContext.BACKEND_ENDPOINT; } + if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) { + delete newContext.MONGO_PROXY_ENDPOINT; + } + if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) { delete newContext.MONGO_BACKEND_ENDPOINT; } + if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) { + delete newContext.CASSANDRA_PROXY_ENDPOINT; + } + if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) { delete newContext.JUNO_ENDPOINT; } @@ -148,10 +189,7 @@ export function updateConfigContext(newContext: Partial): void { // Injected for local development. These will be removed in the production bundle by webpack if (process.env.NODE_ENV === "development") { - const port: string = process.env.PORT || "1234"; updateConfigContext({ - BACKEND_ENDPOINT: "https://localhost:" + port, - MONGO_BACKEND_ENDPOINT: "https://localhost:" + port, PROXY_PATH: "/proxy", EMULATOR_ENDPOINT: "https://localhost:8081", }); diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index ac7b9499e..3f7e3a7db 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,4 +1,4 @@ -import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; +import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; export interface ArmEntity { id: string; @@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties { ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; capacity?: { totalThroughputLimit: number }; + capacityMode?: CapacityMode; locations?: DatabaseAccountResponseLocation[]; postgresqlEndpoint?: string; publicNetworkAccess?: string; @@ -157,8 +158,10 @@ export interface Collection extends Resource { changeFeedPolicy?: ChangeFeedPolicy; analyticalStorageTtl?: number; geospatialConfig?: GeospatialConfig; + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; schema?: ISchema; requestSchema?: () => void; + computedProperties?: ComputedProperties; } export interface CollectionsWithPagination { @@ -193,10 +196,23 @@ export interface IndexingPolicy { indexingMode: "consistent" | "lazy" | "none"; includedPaths: any; excludedPaths: any; - compositeIndexes?: any; - spatialIndexes?: any; + compositeIndexes?: any[]; + spatialIndexes?: any[]; + vectorIndexes?: VectorIndex[]; } +export interface VectorIndex { + path: string; + type: "flat" | "diskANN" | "quantizedFlat"; +} + +export interface ComputedProperty { + name: string; + query: string; +} + +export type ComputedProperties = ComputedProperty[]; + export interface PartitionKey { paths: string[]; kind: "Hash" | "Range" | "MultiHash"; @@ -325,6 +341,18 @@ export interface CreateCollectionParams { partitionKey?: PartitionKey; uniqueKeyPolicy?: UniqueKeyPolicy; createMongoWildcardIndex?: boolean; + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; +} + +export interface VectorEmbeddingPolicy { + vectorEmbeddings: VectorEmbedding[]; +} + +export interface VectorEmbedding { + dataType: "float16" | "float32" | "uint8" | "int8"; + dimensions: number; + distanceFunction: "euclidean" | "cosine" | "dotproduct"; + path: string; } export interface ReadDatabaseOfferParams { diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts index 42e5d3dc0..6e49b0555 100644 --- a/src/Contracts/ExplorerContracts.ts +++ b/src/Contracts/ExplorerContracts.ts @@ -1,6 +1,6 @@ -import { MessageTypes } from "Contracts/MessageTypes"; import * as ActionContracts from "./ActionContracts"; import * as Diagnostics from "./Diagnostics"; +import { MessageTypes } from "./MessageTypes"; import * as Versions from "./Versions"; export { ActionContracts, Diagnostics, MessageTypes, Versions }; diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index d0b36f1c6..b381fd845 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -53,6 +53,7 @@ export type FabricMessageV2 = id: string; message: { connectionId: string; + isVisible: boolean; }; } | { @@ -72,7 +73,7 @@ export type FabricMessageV2 = }; } | { - type: "setToolbarStatus"; + type: "explorerVisible"; message: { visible: boolean; }; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 7521647df..66fd54ec4 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -135,6 +135,7 @@ export interface Collection extends CollectionBase { changeFeedPolicy: ko.Observable; geospatialConfig: ko.Observable; documentIds: ko.ObservableArray; + computedProperties: ko.Observable; cassandraKeys: CassandraTableKeys; cassandraSchema: CassandraTableKey[]; @@ -386,9 +387,11 @@ export interface DataExplorerInputsFrame { dnsSuffix?: string; serverId?: string; extensionEndpoint?: string; + portalBackendEndpoint?: string; + mongoProxyEndpoint?: string; + cassandraProxyEndpoint?: string; subscriptionType?: SubscriptionType; quotaId?: string; - addCollectionDefaultFlight?: string; isTryCosmosDBSubscription?: boolean; loadDatabaseAccountTimestamp?: number; sharedThroughputMinimum?: number; @@ -406,6 +409,7 @@ export interface DataExplorerInputsFrame { features?: { [key: string]: string; }; + feedbackPolicies?: any; } export interface SelfServeFrameInputs { diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index d943f7cf0..207642d84 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -26,8 +26,8 @@ export class CollapsibleSectionComponent extends React.Component void; + onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void; /** * Label for the button @@ -107,10 +108,17 @@ export interface CommandButtonComponentProps { * Vertical bar to divide buttons */ isDivider?: boolean; + /** * Aria-label for the button */ ariaLabel: string; + + /** + * If specified, a keyboard action that should trigger this button's onCommandClick handler when activated. + * If not specified, the button will not be triggerable by keyboard shortcuts. + */ + keyboardAction?: KeyboardAction; } export class CommandButtonComponent extends React.Component { diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 956253a05..a6f487ba5 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -20,7 +20,10 @@ export interface EditorReactProps { lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"]; minimap?: monaco.editor.IEditorOptions["minimap"]; scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"]; + fontSize?: monaco.editor.IEditorOptions["fontSize"]; monacoContainerStyles?: React.CSSProperties; + className?: string; + spinnerClassName?: string; } export class EditorReact extends React.Component { @@ -46,9 +49,25 @@ export class EditorReact extends React.Component - {!this.state.showEditor && } + {!this.state.showEditor && ( + + )}
this.setRef(elt)} /> @@ -71,9 +92,14 @@ export class EditorReact extends React.Component { + // Hooking the model's onDidChangeContent event because of some event ordering issues. + // If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely), + // then there are some inconsistencies as to which event fires first. + // But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event. + // (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.) + // If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first. + this.editor.onDidChangeModelContent(() => { const queryEditorModel = this.editor.getModel(); this.props.onContentChanged(queryEditorModel.getValue()); }); @@ -98,7 +124,7 @@ export class EditorReact extends React.Component this.setState({ isMongoIndexingPolicyDiscardable }); + private onComputedPropertiesContentChange = (newComputedProperties: DataModels.ComputedProperties): void => + this.setState({ computedPropertiesContent: newComputedProperties }); + + private resetShouldDiscardComputedProperties = (): void => this.setState({ shouldDiscardComputedProperties: false }); + + private logComputedPropertiesSuccessMessage = (): void => { + if (this.props.settingsTab.onLoadStartKey) { + traceSuccess( + Action.Tab, + { + databaseName: this.collection.databaseId, + collectionName: this.collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.settingsTab.tabTitle(), + }, + this.props.settingsTab.onLoadStartKey, + ); + this.props.settingsTab.onLoadStartKey = undefined; + } + }; + + private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void => + this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty }); + private calculateTotalThroughputUsed = (): void => { this.totalThroughputUsed = 0; (useDatabases.getState().databases || []).forEach(async (database) => { @@ -636,7 +696,6 @@ export class SettingsComponent extends React.Component => { const newCollection: DataModels.Collection = { ...this.collection.rawDataModel }; - if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) { + if ( + this.state.isSubSettingsSaveable || + this.state.isIndexingPolicyDirty || + this.state.isConflictResolutionDirty || + this.state.isComputedPropertiesDirty + ) { let defaultTtl: number; switch (this.state.timeToLive) { case TtlType.On: @@ -825,6 +897,10 @@ export class SettingsComponent extends React.Component, }); + if (this.isVectorSearchEnabled) { + tabs.push({ + tab: SettingsV2TabTypes.ContainerVectorPolicyTab, + content: , + }); + } + if (this.shouldShowIndexingPolicyEditor) { tabs.push({ tab: SettingsV2TabTypes.IndexingPolicyTab, @@ -1091,6 +1197,20 @@ export class SettingsComponent extends React.Component, + }); + } + + if (this.shouldShowComputedPropertiesEditor) { + tabs.push({ + tab: SettingsV2TabTypes.ComputedPropertiesTab, + content: , + }); + } + const pivotProps: IPivotProps = { onLinkClick: this.onPivotChange, selectedKey: SettingsV2TabTypes[this.state.selectedTab], diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx index 2e66a86f9..06a9d4a0c 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.test.tsx @@ -11,7 +11,6 @@ import { getThroughputApplyLongDelayMessage, getThroughputApplyShortDelayMessage, getToolTipContainer, - indexingPolicynUnsavedWarningMessage, manualToAutoscaleDisclaimerElement, mongoIndexTransformationRefreshingMessage, mongoIndexingPolicyAADError, @@ -39,7 +38,6 @@ class SettingsRenderUtilsTestComponent extends React.Component { {manualToAutoscaleDisclaimerElement} {ttlWarning} - {indexingPolicynUnsavedWarningMessage} {updateThroughputDelayedApplyWarningMessage} {getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)} diff --git a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx index fefd2f6e5..1fe4536f0 100644 --- a/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsRenderUtils.tsx @@ -61,6 +61,8 @@ export interface PriceBreakdown { currencySign: string; } +export type editorType = "indexPolicy" | "computedProperties"; + export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } }; export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { @@ -254,9 +256,10 @@ export const ttlWarning: JSX.Element = ( ); -export const indexingPolicynUnsavedWarningMessage: JSX.Element = ( +export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => ( - You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. + You have not saved the latest changes made to your{" "} + {editor === "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes. ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx new file mode 100644 index 000000000..811bc17ba --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.test.tsx @@ -0,0 +1,56 @@ +import * as DataModels from "Contracts/DataModels"; +import { shallow } from "enzyme"; +import React from "react"; +import { ComputedPropertiesComponent, ComputedPropertiesComponentProps } from "./ComputedPropertiesComponent"; + +describe("ComputedPropertiesComponent", () => { + const initialComputedPropertiesContent: DataModels.ComputedProperties = [ + { + name: "prop1", + query: "query1", + }, + ]; + const baseProps: ComputedPropertiesComponentProps = { + computedPropertiesContent: initialComputedPropertiesContent, + computedPropertiesContentBaseline: initialComputedPropertiesContent, + logComputedPropertiesSuccessMessage: () => { + return; + }, + onComputedPropertiesContentChange: () => { + return; + }, + onComputedPropertiesDirtyChange: () => { + return; + }, + resetShouldDiscardComputedProperties: () => { + return; + }, + shouldDiscardComputedProperties: false, + }; + + it("renders", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("computed properties are reset", () => { + const wrapper = shallow(); + + const computedPropertiesComponentInstance = wrapper.instance() as ComputedPropertiesComponent; + const resetComputedPropertiesEditorMockFn = jest.fn(); + computedPropertiesComponentInstance.resetComputedPropertiesEditor = resetComputedPropertiesEditorMockFn; + + wrapper.setProps({ shouldDiscardComputedProperties: true }); + wrapper.update(); + expect(resetComputedPropertiesEditorMockFn.mock.calls.length).toEqual(1); + }); + + it("dirty is set", () => { + let computedPropertiesComponent = new ComputedPropertiesComponent(baseProps); + expect(computedPropertiesComponent.IsComponentDirty()).toEqual(false); + + const newProps = { ...baseProps, computedPropertiesContent: undefined as DataModels.ComputedProperties }; + computedPropertiesComponent = new ComputedPropertiesComponent(newProps); + expect(computedPropertiesComponent.IsComponentDirty()).toEqual(true); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx new file mode 100644 index 000000000..c8650b988 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx @@ -0,0 +1,128 @@ +import { FontIcon, Link, MessageBar, MessageBarType, Stack, Text } from "@fluentui/react"; +import * as DataModels from "Contracts/DataModels"; +import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils"; +import { isDirty } from "Explorer/Controls/Settings/SettingsUtils"; +import { loadMonaco } from "Explorer/LazyMonaco"; +import * as monaco from "monaco-editor"; +import * as React from "react"; + +export interface ComputedPropertiesComponentProps { + computedPropertiesContent: DataModels.ComputedProperties; + computedPropertiesContentBaseline: DataModels.ComputedProperties; + logComputedPropertiesSuccessMessage: () => void; + onComputedPropertiesContentChange: (newComputedProperties: DataModels.ComputedProperties) => void; + onComputedPropertiesDirtyChange: (isComputedPropertiesDirty: boolean) => void; + resetShouldDiscardComputedProperties: () => void; + shouldDiscardComputedProperties: boolean; +} + +interface ComputedPropertiesComponentState { + computedPropertiesContentIsValid: boolean; +} + +export class ComputedPropertiesComponent extends React.Component< + ComputedPropertiesComponentProps, + ComputedPropertiesComponentState +> { + private shouldCheckComponentIsDirty = true; + private computedPropertiesDiv = React.createRef(); + private computedPropertiesEditor: monaco.editor.IStandaloneCodeEditor; + + constructor(props: ComputedPropertiesComponentProps) { + super(props); + this.state = { + computedPropertiesContentIsValid: true, + }; + } + + componentDidUpdate(): void { + if (this.props.shouldDiscardComputedProperties) { + this.resetComputedPropertiesEditor(); + this.props.resetShouldDiscardComputedProperties(); + } + this.onComponentUpdate(); + } + + componentDidMount(): void { + this.resetComputedPropertiesEditor(); + this.onComponentUpdate(); + } + + public resetComputedPropertiesEditor = (): void => { + if (!this.computedPropertiesEditor) { + this.createComputedPropertiesEditor(); + } else { + const indexingPolicyEditorModel = this.computedPropertiesEditor.getModel(); + const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4); + indexingPolicyEditorModel.setValue(value); + } + this.onComponentUpdate(); + }; + + private onComponentUpdate = (): void => { + if (!this.shouldCheckComponentIsDirty) { + this.shouldCheckComponentIsDirty = true; + return; + } + this.props.onComputedPropertiesDirtyChange(this.IsComponentDirty()); + this.shouldCheckComponentIsDirty = false; + }; + + public IsComponentDirty = (): boolean => { + if ( + isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && + this.state.computedPropertiesContentIsValid + ) { + return true; + } + + return false; + }; + + private async createComputedPropertiesEditor(): Promise { + const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4); + const monaco = await loadMonaco(); + this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, { + value: value, + language: "json", + ariaLabel: "Computed properties", + }); + if (this.computedPropertiesEditor) { + const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel(); + computedPropertiesEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this)); + this.props.logComputedPropertiesSuccessMessage(); + } + } + + private onEditorContentChange = (): void => { + const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel(); + try { + const newComputedPropertiesContent = JSON.parse( + computedPropertiesEditorModel.getValue(), + ) as DataModels.ComputedProperties; + this.props.onComputedPropertiesContentChange(newComputedPropertiesContent); + this.setState({ computedPropertiesContentIsValid: true }); + } catch (e) { + this.setState({ computedPropertiesContentIsValid: false }); + } + }; + + public render(): JSX.Element { + return ( + + {isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && ( + + {unsavedEditorWarningMessage("computedProperties")} + + )} + + + {"Learn more"} + +   about how to define computed properties and how to use them. + +
+
+ ); + } +} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx new file mode 100644 index 000000000..ca4d63a04 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx @@ -0,0 +1,30 @@ +import { Stack } from "@fluentui/react"; +import { VectorEmbeddingPolicy } from "Contracts/DataModels"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils"; +import React from "react"; + +export interface ContainerVectorPolicyComponentProps { + vectorEmbeddingPolicy: VectorEmbeddingPolicy; +} + +export const ContainerVectorPolicyComponent: React.FC = ({ + vectorEmbeddingPolicy, +}) => { + return ( + + + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 1f216b241..bc5de93a4 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -3,7 +3,7 @@ import * as monaco from "monaco-editor"; import * as React from "react"; import * as DataModels from "../../../../Contracts/DataModels"; import { loadMonaco } from "../../../LazyMonaco"; -import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; +import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils"; import { isDirty, isIndexTransforming } from "../SettingsUtils"; import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; @@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps { logIndexingPolicySuccessMessage: () => void; indexTransformationProgress: number; refreshIndexTransformationProgress: () => Promise; + isVectorSearchEnabled?: boolean; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; } @@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component< indexTransformationProgress={this.props.indexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} /> - {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( - {indexingPolicynUnsavedWarningMessage} + {this.props.isVectorSearchEnabled && ( + + Container vector policies and vector indexes are not modifiable after container creation + )} -
+ {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( + {unsavedEditorWarningMessage("indexPolicy")} + )} +
); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx index c3b09286e..a55630532 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -19,7 +19,6 @@ import { addMongoIndexStackProps, createAndAddMongoIndexStackProps, customDetailsListStyles, - indexingPolicynUnsavedWarningMessage, infoAndToolTipTextStyle, mediumWidthStackStyles, mongoCompoundIndexNotSupportedMessage, @@ -27,15 +26,16 @@ import { onRenderRow, separatorStyles, subComponentStackProps, + unsavedEditorWarningMessage, } from "../../SettingsRenderUtils"; import { AddMongoIndexProps, - getMongoIndexType, - getMongoIndexTypeText, - isIndexTransforming, MongoIndexIdField, MongoIndexTypes, MongoNotificationType, + getMongoIndexType, + getMongoIndexTypeText, + isIndexTransforming, } from "../../SettingsUtils"; import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; @@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component = ({ database, collection, explorer }) => { + const { dataTransferJobs } = useDataTransferJobs(); + const [portalDataTransferJob, setPortalDataTransferJob] = React.useState(null); + + React.useEffect(() => { + const loadDataTransferJobs = refreshDataTransferOperations; + loadDataTransferJobs(); + }, []); + + React.useEffect(() => { + const currentJob = findPortalDataTransferJob(); + setPortalDataTransferJob(currentJob); + startPollingforUpdate(currentJob); + }, [dataTransferJobs]); + + const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash"; + + const getPartitionKeyValue = (): string => { + return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", "); + }; + + const partitionKeyName = "Partition key"; + const partitionKeyValue = getPartitionKeyValue(); + + const textHeadingStyle = { + root: { fontWeight: FontWeights.semibold, fontSize: 16 }, + }; + + const textSubHeadingStyle = { + root: { fontWeight: FontWeights.semibold }, + }; + + const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => { + if (isCurrentJobInProgress(currentJob)) { + const jobName = currentJob?.properties?.jobName; + try { + pollDataTransferJob( + jobName, + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + ); + } catch (error) { + handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`); + } + } + }; + + const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => { + await cancelDataTransferJob( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + currentJob?.properties?.jobName, + ); + }; + + const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => { + const jobStatus = currentJob?.properties?.status; + return ( + jobStatus && + jobStatus !== "Completed" && + jobStatus !== "Cancelled" && + jobStatus !== "Failed" && + jobStatus !== "Faulted" + ); + }; + + const refreshDataTransferOperations = async () => { + await refreshDataTransferJobs( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + ); + }; + + const findPortalDataTransferJob = (): DataTransferJobGetResults => { + return dataTransferJobs.find((feed: DataTransferJobGetResults) => { + const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties + ?.source as CosmosSqlDataTransferDataSourceSink; + return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id(); + }); + }; + + const getProgressDescription = (): string => { + const processedCount = portalDataTransferJob?.properties?.processedCount; + const totalCount = portalDataTransferJob?.properties?.totalCount; + const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : ""; + return `${portalDataTransferJob?.properties?.status} ${processedCountString}`; + }; + + const startPartitionkeyChangeWorkflow = () => { + useSidePanel + .getState() + .openSidePanel( + "Change partition key", + , + ); + }; + + const getPercentageComplete = () => { + const jobStatus = portalDataTransferJob?.properties?.status; + const isCompleted = jobStatus === "Completed"; + if (isCompleted) { + return 1; + } + const processedCount = portalDataTransferJob?.properties?.processedCount; + const totalCount = portalDataTransferJob?.properties?.totalCount; + const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob); + return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0; + }; + + return ( + + + Change {partitionKeyName.toLowerCase()} + + + Current {partitionKeyName.toLowerCase()} + Partitioning + + + {partitionKeyValue} + {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} + + + + + To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the + source container for the entire duration of the partition key change process. + + Learn more + + + + To change the partition key, a new destination container must be created or an existing destination container + selected. Data will then be copied to the destination container. + + + {portalDataTransferJob && ( + + {partitionKeyName} change job + + + {isCurrentJobInProgress(portalDataTransferJob) && ( + cancelRunningDataTransferJob(portalDataTransferJob)} /> + )} + + + )} + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 6346e8733..664ae01c7 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< }; const costElement = (): JSX.Element => { - const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true); + const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false); return ( {newThroughput && newThroughputCostElement()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 6a0f9efd1..605732141 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = ` > $ - 0.012 + 0.0080 /hr $ - 0.29 + 0.19 /day $ - 8.76 + 5.84 /mo @@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` > $ - 0.012 + 0.0080 /hr $ - 0.29 + 0.19 /day $ - 8.76 + 5.84 /mo diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap new file mode 100644 index 000000000..2f67d7d70 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ComputedPropertiesComponent renders 1`] = ` + + + + Learn more + + + +   about how to define computed properties and how to use them. + +
+ +`; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap index 1f66324f5..93516dd7e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap @@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = ` refreshIndexTransformationProgress={[Function]} />
diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index a533b6446..cff7d1f74 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; const zeroValue = 0; -export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy; +export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties; export const TtlOff = "off"; export const TtlOn = "on"; export const TtlOnNoDefault = "on-nodefault"; @@ -45,6 +45,9 @@ export enum SettingsV2TabTypes { ConflictResolutionTab, SubSettingsTab, IndexingPolicyTab, + PartitionKeyTab, + ComputedPropertiesTab, + ContainerVectorPolicyTab, } export interface IsComponentDirtyResult { @@ -146,6 +149,12 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { return "Settings"; case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; + case SettingsV2TabTypes.PartitionKeyTab: + return "Partition Keys (preview)"; + case SettingsV2TabTypes.ComputedPropertiesTab: + return "Computed Properties"; + case SettingsV2TabTypes.ContainerVectorPolicyTab: + return "Container Vector Policy (preview)"; default: throw new Error(`Unknown tab ${tab}`); } @@ -199,3 +208,49 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => { export const isIndexTransforming = (indexTransformationProgress: number): boolean => // index transformation progress can be 0 indexTransformationProgress !== undefined && indexTransformationProgress !== 100; + +export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => { + const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key"; + return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName; +}; + +export const getPartitionKeyTooltipText = (apiType: string): string => { + if (apiType === "Mongo") { + return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data."; + } + let tooltipText = `The ${getPartitionKeyName( + apiType, + true, + )} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`; + if (apiType === "SQL") { + tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."; + } + return tooltipText; +}; + +export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => { + if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) { + const subtext = "For small workloads, the item ID is a suitable choice for the partition key."; + return subtext; + } + return ""; +}; + +export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => { + switch (apiType) { + case "Mongo": + return "e.g., categoryId"; + case "Gremlin": + return "e.g., /address"; + case "SQL": + return `${ + index === undefined + ? "Required - first partition key e.g., /TenantId" + : index === 0 + ? "second partition key e.g., /UserId" + : "third partition key e.g., /SessionId" + }`; + default: + return "e.g., /address/zipCode"; + } +}; diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index 41b11ca68..d0c794025 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -40,6 +40,12 @@ export const collection = { version: 2, }, partitionKeyProperties: ["partitionKey"], + computedProperties: ko.observable([ + { + name: "queryName", + query: "query", + }, + ]), readSettings: () => { return; }, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 5e905e786..561368bdd 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -26,10 +26,10 @@ exports[`SettingsComponent renders 1`] = ` Object { "analyticalStorageTtl": [Function], "changeFeedPolicy": [Function], + "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -103,10 +103,10 @@ exports[`SettingsComponent renders 1`] = ` Object { "analyticalStorageTtl": [Function], "changeFeedPolicy": [Function], + "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -196,6 +196,7 @@ exports[`SettingsComponent renders 1`] = ` "indexingMode": "consistent", } } + isVectorSearchEnabled={false} logIndexingPolicySuccessMessage={[Function]} onIndexingPolicyContentChange={[Function]} onIndexingPolicyDirtyChange={[Function]} @@ -204,6 +205,131 @@ exports[`SettingsComponent renders 1`] = ` shouldDiscardIndexingPolicy={false} /> + + + + + +
diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 004862ffe..5a71353cd 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -99,18 +99,6 @@ exports[`SettingsUtils functions render 1`] = ` . - - You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. - :not(:last-child) { margin-bottom: @DefaultSpace; } -.capacitycalculator-link:focus{ + +.capacitycalculator-link:focus { text-decoration: underline; outline-offset: 2px; -} \ No newline at end of file +} + +.copyQuery:focus::after, +.deleteQuery:focus::after { + outline: none !important; +} diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index 362390b80..4cf601047 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -247,7 +247,7 @@ export class TreeNodeComponent extends React.Component { + sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); + Logger.logInfo( + `CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`, + "Explorer/openCESCVAFeedbackBlade", + ); } public async refreshDatabaseForResourceToken(): Promise { @@ -528,104 +508,6 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } - public resetNotebookWorkspace(): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace", - ); - return; - } - const dialogContent = useNotebook.getState().isPhoenixNotebooks - ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" - : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; - - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - title: "Reset Workspace", - subText: dialogContent, - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: () => useDialog.getState().closeDialog(), - }; - useDialog.getState().openDialog(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - try { - const { value: workspaces } = await listByDatabaseAccount( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - ); - return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private _resetNotebookWorkspace = async () => { - useDialog.getState().closeDialog(); - const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); - let connectionStatus: ContainerConnectionInfo; - try { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - logConsoleError(error); - return; - } - TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - if (useNotebook.getState().isPhoenixNotebooks) { - useTabs.getState().closeAllNotebookTabs(true); - connectionStatus = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - } - const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo?.status !== HttpStatusCodes.OK) { - throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); - } - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.setNotebookInfo(true, connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - if (useNotebook.getState().isPhoenixNotebooks) { - connectionStatus = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - throw error; - } finally { - clearInProgressMessage(); - } - }; - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -1028,92 +910,6 @@ export default class Explorer { ); } - /** - * This creates a new notebook file, then opens the notebook - */ - public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - if (useNotebook.getState().isPhoenixNotebooks) { - if (isGithubTree) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } else { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookModalTitle, - undefined, - "Create", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } - } - - private getNewNoteWarningText(): JSX.Element { - return ( - <> -

{Notebook.newNotebookModalContent1}

-
-

- {Notebook.newNotebookModalContent2} - - {Notebook.learnMore} - -

- - ); - } - - private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { - const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - dataExplorerArea: Constants.Areas.Notebook, - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent, isGithubTree) - .then((newFile: NotebookContentItem) => { - logConsoleInfo(`Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - logConsoleError(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }) - .finally(clearInProgressMessage); - } - // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -1148,10 +944,6 @@ export default class Explorer { let title: string; switch (kind) { - case ViewModels.TerminalKind.Default: - title = "Terminal"; - break; - case ViewModels.TerminalKind.Mongo: title = "Mongo Shell"; break; @@ -1305,36 +1097,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenixNotebooks) { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookUploadModalTitle, - undefined, - "Upload", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - } - } - - private uploadFilePanel(parent?: NotebookContentItem): void { - useSidePanel - .getState() - .openSidePanel( - "Upload file to notebook server", - this.uploadFile(name, content, parent)} />, - ); - } - public getDownloadModalConent(fileName: string): JSX.Element { if (useNotebook.getState().isPhoenixNotebooks) { return ( diff --git a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx index 6b0472649..f67425696 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx @@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component< onActivated={this.setIsDeleteConfirm.bind(this, true)} aria-label="Delete this vertex" > - Delete + Delete ); } else { @@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component< aria-label="Edit properties" onActivated={expandClickHandler} > - Edit + Edit )} diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx index de357989a..6b20cfcb0 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx @@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent = ( className="rightPaneTrashIcon rightPaneBtns" tabIndex={0} role="button" + aria-label={`Delete ${data.key}`} onClick={(event: React.MouseEvent) => removeNewVertexProperty(event, index)} onKeyPress={(event: React.KeyboardEvent) => removeNewVertexPropertyKeyPress(event, index) } > - Remove property + Remove property
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 141bda577..9a5f222a3 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -5,6 +5,7 @@ */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { useNotebook } from "Explorer/Notebook/useNotebook"; +import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; @@ -40,6 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -105,6 +107,10 @@ export const CommandBar: React.FC = ({ container }: Props) => { }, }; + const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); + const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); + setKeyboardHandlers(keyboardHandlers); + return (
{ }); }); - describe("Enable notebook button", () => { - const enableNotebookBtnLabel = "Enable Notebooks (Preview)"; - const selectedNodeState = useSelectedNode.getState(); - - beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ - portalEnv: "prod", - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - }); - - afterEach(() => { - updateUserContext({ - portalEnv: "prod", - }); - useNotebook.getState().setIsNotebookEnabled(false); - useNotebook.getState().setIsNotebooksEnabledForAccount(false); - }); - - it("Notebooks is already enabled - button should be hidden", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - expect(enableNotebookBtn).toBeUndefined(); - }); - - it("Account is running on one of the national clouds - button should be hidden", () => { - updateUserContext({ - portalEnv: "mooncake", - }); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - expect(enableNotebookBtn).toBeUndefined(); - }); - - it("Notebooks is not enabled but is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - - //TODO: modify once notebooks are available - expect(enableNotebookBtn).toBeUndefined(); - //expect(enableNotebookBtn).toBeDefined(); - //expect(enableNotebookBtn.disabled).toBe(false); - //expect(enableNotebookBtn.tooltipText).toBe(""); - }); - - it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); - - //TODO: modify once notebooks are available - expect(enableNotebookBtn).toBeUndefined(); - //expect(enableNotebookBtn).toBeDefined(); - //expect(enableNotebookBtn.disabled).toBe(true); - //expect(enableNotebookBtn.tooltipText).toBe( - // "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks." - //); - }); - }); - - describe("Open Mongo shell button", () => { - const openMongoShellBtnLabel = "Open Mongo shell"; - const selectedNodeState = useSelectedNode.getState(); - - beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - }); - - afterAll(() => { - updateUserContext({ - apiType: "SQL", - }); - useNotebook.getState().setIsShellEnabled(false); - }); - - beforeEach(() => { - updateUserContext({ - apiType: "Mongo", - }); - useNotebook.getState().setIsShellEnabled(true); - }); - - afterEach(() => { - useNotebook.getState().setIsNotebookEnabled(false); - useNotebook.getState().setIsNotebooksEnabledForAccount(false); - }); - - it("Mongo Api not available - button should be hidden", () => { - updateUserContext({ - apiType: "SQL", - }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Running on a national cloud - button should be hidden", () => { - updateUserContext({ - portalEnv: "mooncake", - }); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Notebooks is not enabled and is unavailable - button should be hidden", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Notebooks is not enabled and is available - button should be hidden", () => { - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - - it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openMongoShellBtn.disabled).toBe(true); - //expect(openMongoShellBtn.disabled).toBe(false); - //expect(openMongoShellBtn.tooltipText).toBe(""); - }); - - it("Notebooks is enabled and is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openMongoShellBtn.disabled).toBe(true); - //expect(openMongoShellBtn.disabled).toBe(false); - //expect(openMongoShellBtn.tooltipText).toBe(""); - }); - - it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - useNotebook.getState().setIsShellEnabled(false); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeUndefined(); - }); - }); - describe("Open Cassandra shell button", () => { const openCassandraShellBtnLabel = "Open Cassandra shell"; const selectedNodeState = useSelectedNode.getState(); @@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => { const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); - - it("Notebooks is not enabled and is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - expect(openCassandraShellBtn).toBeUndefined(); - }); - - it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - - expect(openCassandraShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openCassandraShellBtn.disabled).toBe(true); - //expect(openCassandraShellBtn.disabled).toBe(false); - //expect(openCassandraShellBtn.tooltipText).toBe(""); - }); - - it("Notebooks is enabled and is available - button should be shown and enabled", () => { - useNotebook.getState().setIsNotebookEnabled(true); - useNotebook.getState().setIsNotebooksEnabledForAccount(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - expect(openCassandraShellBtn).toBeDefined(); - - //TODO: modify once notebooks are available - expect(openCassandraShellBtn.disabled).toBe(true); - //expect(openCassandraShellBtn.disabled).toBe(false); - //expect(openCassandraShellBtn.tooltipText).toBe(""); - }); }); describe("Open Postgres and vCore Mongo buttons", () => { @@ -368,62 +155,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }); }); - describe("GitHub buttons", () => { - const connectToGitHubBtnLabel = "Connect to GitHub"; - const manageGitHubSettingsBtnLabel = "Manage GitHub settings"; - const selectedNodeState = useSelectedNode.getState(); - - beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableTable" }], - }, - } as DatabaseAccount, - }); - - mockExplorer.notebookManager = new NotebookManager(); - mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined); - }); - - afterEach(() => { - jest.resetAllMocks(); - useNotebook.getState().setIsNotebookEnabled(false); - }); - - it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => { - useNotebook.getState().setIsNotebookEnabled(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); - expect(connectToGitHubBtn).toBeDefined(); - }); - - it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => { - useNotebook.getState().setIsNotebookEnabled(true); - mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - const manageGitHubSettingsBtn = buttons.find( - (button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel, - ); - expect(manageGitHubSettingsBtn).toBeDefined(); - }); - - it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); - - const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); - expect(connectToGitHubBtn).toBeUndefined(); - - const manageGitHubSettingsBtn = buttons.find( - (button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel, - ); - expect(manageGitHubSettingsBtn).toBeUndefined(); - }); - }); - describe("Resource token", () => { const mockCollection = { id: ko.observable("test") } as CollectionBase; useSelectedNode.getState().setSelectedNode(mockCollection); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 78810e9db..a1aa3e49b 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -1,3 +1,5 @@ +import { KeyboardAction } from "KeyboardShortcuts"; +import { ReactTabKind, useTabs } from "hooks/useTabs"; import * as React from "react"; import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddDatabaseIcon from "../../../../images/AddDatabase.svg"; @@ -6,13 +8,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg"; import AddTriggerIcon from "../../../../images/AddTrigger.svg"; import AddUdfIcon from "../../../../images/AddUdf.svg"; import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg"; -import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg"; +import HomeIcon from "../../../../images/Home_16.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg"; -import GitHubIcon from "../../../../images/github.svg"; -import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg"; -import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import SettingsIcon from "../../../../images/settings_15x15.svg"; import SynapseIcon from "../../../../images/synapse-link.svg"; @@ -20,7 +19,6 @@ import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { Platform, configContext } from "../../../ConfigContext"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { JunoClient } from "../../../Juno/JunoClient"; import { userContext } from "../../../UserContext"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; @@ -31,7 +29,6 @@ import { useNotebook } from "../../Notebook/useNotebook"; import { OpenFullScreen } from "../../OpenFullScreen"; import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; -import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane"; import { useDatabases } from "../../useDatabases"; @@ -57,7 +54,11 @@ export function createStaticCommandBarButtons( }; if (configContext.platform !== Platform.Fabric) { + const homeBtn = createHomeButton(); + buttons.push(homeBtn); + const newCollectionBtn = createNewCollectionGroup(container); + newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below. buttons.push(newCollectionBtn); if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { const addSynapseLink = createOpenSynapseLinkDialogButton(container); @@ -75,57 +76,6 @@ export function createStaticCommandBarButtons( } } - if (useNotebook.getState().isNotebookEnabled) { - addDivider(); - const notebookButtons: CommandButtonComponentProps[] = []; - - const newNotebookButton = createNewNotebookButton(container); - newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; - notebookButtons.push(newNotebookButton); - - if (container.notebookManager?.gitHubOAuthService) { - notebookButtons.push(createManageGitHubAccountButton(container)); - } - if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) { - notebookButtons.push(createOpenTerminalButton(container)); - } - if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) { - notebookButtons.push(createNotebookWorkspaceResetButton(container)); - } - if ( - (userContext.apiType === "Mongo" && - useNotebook.getState().isShellEnabled && - selectedNodeState.isDatabaseNodeOrNoneSelected()) || - userContext.apiType === "Cassandra" - ) { - notebookButtons.push(createDivider()); - if (userContext.apiType === "Cassandra") { - notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra)); - } else { - notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo)); - } - } - - notebookButtons.forEach((btn) => { - if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { - if (!useNotebook.getState().isPhoenixFeatures) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg); - } - } else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) { - if (!useNotebook.getState().isPhoenixFeatures) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg); - } - } else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) { - if (!useNotebook.getState().isPhoenixFeatures) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg); - } - } else if (!useNotebook.getState().isPhoenixNotebooks) { - applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg); - } - buttons.push(btn); - }); - } - if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) { const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; @@ -135,7 +85,7 @@ export function createStaticCommandBarButtons( buttons.push(newSqlQueryBtn); } - if (isQuerySupported && selectedNodeState.findSelectedCollection()) { + if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) { const openQueryBtn = createOpenQueryButton(container); openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; buttons.push(openQueryBtn); @@ -146,6 +96,7 @@ export function createStaticCommandBarButtons( const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_SPROC, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); @@ -196,18 +147,22 @@ export function createContextCommandBarButtons( } export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = [ - { - iconSrc: SettingsIcon, - iconAlt: "Settings", - onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", ), - commandButtonLabel: undefined, - ariaLabel: "Settings", - tooltipText: "Settings", - hasPopup: true, - disabled: false, - }, - ]; + const buttons: CommandButtonComponentProps[] = + configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly + ? [] + : [ + { + iconSrc: SettingsIcon, + iconAlt: "Settings", + onCommandClick: () => + useSidePanel.getState().openSidePanel("Settings", ), + commandButtonLabel: undefined, + ariaLabel: "Settings", + tooltipText: "Settings", + hasPopup: true, + disabled: false, + }, + ]; const showOpenFullScreen = configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin"; @@ -231,12 +186,12 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt buttons.push(fullScreenButton); } - if (configContext.platform !== Platform.Emulator) { + if (configContext.platform === Platform.Portal) { const label = "Feedback"; const feedbackButtonOptions: CommandButtonComponentProps = { iconSrc: FeedbackIcon, iconAlt: label, - onCommandClick: () => container.provideFeedbackEmail(), + onCommandClick: () => container.openCESCVAFeedbackBlade(), commandButtonLabel: undefined, ariaLabel: label, tooltipText: label, @@ -281,6 +236,18 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr }; } +function createHomeButton(): CommandButtonComponentProps { + const label = "Home"; + return { + iconSrc: HomeIcon, + iconAlt: label, + onCommandClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Home), + commandButtonLabel: label, + hasPopup: false, + ariaLabel: label, + }; +} + function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps { if (configContext.platform === Platform.Emulator) { return undefined; @@ -313,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { return { iconSrc: AddDatabaseIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_DATABASE, onCommandClick: async () => { const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; if (throughputCap && throughputCap !== -1) { @@ -333,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB id: "newQueryBtn", iconSrc: AddSqlQueryIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_QUERY, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); @@ -348,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB id: "newQueryBtn", iconSrc: AddSqlQueryIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_QUERY, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); @@ -373,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_SPROC, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); @@ -392,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newUserDefinedFunctionBtn: CommandButtonComponentProps = { iconSrc: AddUdfIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_UDF, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); @@ -411,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newTriggerBtn: CommandButtonComponentProps = { iconSrc: AddTriggerIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_TRIGGER, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); @@ -428,45 +401,12 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) return buttons; } -function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void { - if (!buttonProps.isDivider) { - buttonProps.disabled = true; - buttonProps.tooltipText = tooltip; - } -} - -function createNewNotebookButton(container: Explorer): CommandButtonComponentProps { - const label = "New Notebook"; - return { - id: "newNotebookBtn", - iconSrc: NewNotebookIcon, - iconAlt: label, - onCommandClick: () => container.onNewNotebookClicked(), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - -function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps { - const label = "Upload to Notebook Server"; - return { - iconSrc: NewNotebookIcon, - iconAlt: label, - onCommandClick: () => container.openUploadFilePanel(), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - function createOpenQueryButton(container: Explorer): CommandButtonComponentProps { const label = "Open Query"; return { iconSrc: BrowseQueriesIcon, iconAlt: label, + keyboardAction: KeyboardAction.OPEN_QUERY, onCommandClick: () => useSidePanel.getState().openSidePanel("Open Saved Queries", ), commandButtonLabel: label, @@ -481,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps { return { iconSrc: OpenQueryFromDiskIcon, iconAlt: label, + keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK, onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", ), commandButtonLabel: label, ariaLabel: label, @@ -489,19 +430,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps { }; } -function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps { - const label = "Open Terminal"; - return { - iconSrc: CosmosTerminalIcon, - iconAlt: label, - onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - function createOpenTerminalButtonByKind( container: Explorer, terminalKind: ViewModels.TerminalKind, @@ -541,45 +469,6 @@ function createOpenTerminalButtonByKind( }; } -function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps { - const label = "Reset Workspace"; - return { - iconSrc: ResetWorkspaceIcon, - iconAlt: label, - onCommandClick: () => container.resetNotebookWorkspace(), - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - -function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps { - const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn(); - const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub"; - const junoClient = new JunoClient(); - return { - iconSrc: GitHubIcon, - iconAlt: label, - onCommandClick: () => { - useSidePanel - .getState() - .openSidePanel( - label, - , - ); - }, - commandButtonLabel: label, - hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), - ariaLabel: label, - }; -} - function createStaticCommandBarButtonsForResourceToken( container: Explorer, selectedNodeState: SelectedNodeState, diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 0b866cbbf..fc67ad894 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -7,6 +7,7 @@ import { IDropdownStyles, } from "@fluentui/react"; import { useQueryCopilot } from "hooks/useQueryCopilot"; +import { KeyboardHandlerMap } from "KeyboardShortcuts"; import * as React from "react"; import _ from "underscore"; import ChevronDownIcon from "../../../../images/Chevron_down.svg"; @@ -37,7 +38,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol if (isDisabled) { return StyleConstants.GrayScale; } - return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined; + return configContext.platform == Platform.Fabric ? StyleConstants.FabricToolbarIconColor : undefined; }; return btns @@ -96,7 +97,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol }, width: 16, }, - label: { fontSize: StyleConstants.mediumFontSize }, + label: { + fontSize: + configContext.platform == Platform.Fabric + ? StyleConstants.DefaultFontSize + : StyleConstants.mediumFontSize, + }, rootHovered: { backgroundColor: hoverColor }, rootPressed: { backgroundColor: hoverColor }, splitButtonMenuButtonExpanded: { @@ -133,7 +139,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol // TODO Figure out how to do it the proper way with subComponentStyles. // TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes selectors: { - ".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize }, + ".ms-ContextualMenu-itemText": { + fontSize: + configContext.platform == Platform.Fabric + ? StyleConstants.DefaultFontSize + : StyleConstants.mediumFontSize, + }, ".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor }, ".ms-ContextualMenu-icon": { width: 16, height: 16 }, }, @@ -223,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, onRender: () => , }; }; + +export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap { + const handlers: KeyboardHandlerMap = {}; + + function createHandlers(buttons: CommandButtonComponentProps[]) { + buttons.forEach((button) => { + if (!button.disabled && button.keyboardAction) { + handlers[button.keyboardAction] = (e) => { + button.onCommandClick(e); + + // If the handler is bound, it means the button is visible and enabled, so we should prevent the default action + return true; + }; + } + + if (button.children && button.children.length > 0) { + createHandlers(button.children); + } + }); + } + + createHandlers(allButtons); + + return handlers; +} diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index b66d344cc..7cefa9ac2 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -162,6 +162,7 @@ export class NotificationConsoleComponent extends React.Component< role="button" onKeyDown={(event: React.KeyboardEvent) => this.onClearNotificationsKeyPress(event)} tabIndex={0} + style={{ border: "1px solid black", borderRadius: "2px" }} > clear notifications image Clear Notifications diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index 8b8b2bdeb..f702107b8 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -146,6 +146,12 @@ exports[`NotificationConsoleComponent renders the console 1`] = ` onClick={[Function]} onKeyDown={[Function]} role="button" + style={ + Object { + "border": "1px solid black", + "borderRadius": "2px", + } + } tabIndex={0} > { - if (!action.collectionResourceId && collections.length === 0) { - subscription.dispose(); - openCollectionTab(action, databases, ++i); - return; - } - - for (let j = 0; j < collections.length; j++) { - const collection: ViewModels.Collection = collections[j]; - if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) { - continue; - } - - // select the collection - collection.expandCollection(); - - if ( - action.tabKind === ActionContracts.TabKind.SQLDocuments || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments] - ) { - collection.onDocumentDBDocumentsClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.MongoDocuments || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments] - ) { - collection.onMongoDBDocumentsClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.SchemaAnalyzer || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] - ) { - collection.onSchemaAnalyzerClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.TableEntities || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] - ) { - collection.onTableEntitiesClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.Graph || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph] - ) { - collection.onGraphDocumentsClick(); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.SQLQuery || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] - ) { - collection.onNewQueryClick( - collection, - undefined, - generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties), - ); - break; - } - - if ( - action.tabKind === ActionContracts.TabKind.ScaleSettings || - action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings] - ) { - collection.onSettingsClick(); - break; - } - } - subscription.dispose(); + //if databases are not yet loaded, wait until loaded + if (!databases || databases.length === 0) { + const databaseActionHandler = (databases: ViewModels.Database[]) => { + databasesUnsubscription(); + openCollectionTab(action, databases, 0); + return; }; + const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases); + } else { + for (let i = initialDatabaseIndex; i < databases.length; i++) { + const database: ViewModels.Database = databases[i]; + if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) { + continue; + } - const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections)); - if (database.collections && database.collections() && database.collections().length) { - collectionActionHandler(database.collections()); + //expand database first if not expanded to load the collections + if (!database.isDatabaseExpanded?.()) { + database.expandDatabase?.(); + } + + const collectionActionHandler = (collections: ViewModels.Collection[]) => { + if (!action.collectionResourceId && collections.length === 0) { + subscription.dispose(); + openCollectionTab(action, databases, ++i); + return; + } + + for (let j = 0; j < collections.length; j++) { + const collection: ViewModels.Collection = collections[j]; + if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) { + continue; + } + + // select the collection + collection.expandCollection(); + + if ( + action.tabKind === ActionContracts.TabKind.SQLDocuments || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments] + ) { + collection.onDocumentDBDocumentsClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.MongoDocuments || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments] + ) { + collection.onMongoDBDocumentsClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.SchemaAnalyzer || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] + ) { + collection.onSchemaAnalyzerClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.TableEntities || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] + ) { + collection.onTableEntitiesClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.Graph || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph] + ) { + collection.onGraphDocumentsClick(); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.SQLQuery || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] + ) { + collection.onNewQueryClick( + collection, + undefined, + generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties), + ); + break; + } + + if ( + action.tabKind === ActionContracts.TabKind.ScaleSettings || + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings] + ) { + collection.onSettingsClick(); + break; + } + } + subscription.dispose(); + }; + + const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections)); + if (database.collections && database.collections() && database.collections().length) { + collectionActionHandler(database.collections()); + } + + break; } - - break; } } diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 2a99a56a7..274bd0d1d 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; import { SubscriptionType } from "Contracts/SubscriptionType"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import React from "react"; @@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import { userContext } from "UserContext"; import { getCollectionName } from "Utils/APITypeUtils"; -import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils"; +import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { getUpsellMessage } from "Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; @@ -81,6 +82,26 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = { excludedPaths: [], }; +const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [ + { + path: "/*", + }, + ], + excludedPaths: [ + { + path: '/"_etag"/?', + }, + ], + vectorIndexes: [], +}; + +export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { + vectorEmbeddings: [], +}; + export interface AddCollectionPanelState { createNewDatabase: boolean; newDatabaseId: string; @@ -101,6 +122,8 @@ export interface AddCollectionPanelState { isExecuting: boolean; isThroughputCapExceeded: boolean; teachingBubbleStep: number; + vectorIndexingPolicy: string; + vectorEmbeddingPolicy: string; } export class AddCollectionPanel extends React.Component { @@ -136,6 +159,8 @@ export class AddCollectionPanel extends React.Component +
{this.state.errorMessage && ( )} - + {this.shouldShowVectorSearchParameters() && ( + + { + this.scrollToSection("collapsibleVectorPolicySectionContent"); + }} + > + + + Learn more + + this.setVectorIndexingPolicy(newIndexingPolicy)} + /> + + + { + this.scrollToSection("collapsibleVectorPolicySectionContent"); + }} + > + + + Learn more + + + this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy) + } + /> + + + + )} {userContext.apiType !== "Tables" && ( { TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); - this.scrollToAdvancedSection(); + this.scrollToSection("collapsibleAdvancedSectionContent"); }} > - + {isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && ( @@ -924,10 +1015,9 @@ export class AddCollectionPanel extends React.Component - To ensure compatibility with - older SDKs, the created container will use a legacy partitioning scheme that supports partition - key values of size only up to 101 bytes. If this is enabled, you will not be able to use - hierarchical partition keys.{" "} + To ensure compatibility with older SDKs, the + created container will use a legacy partitioning scheme that supports partition key values of size + only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "} Learn more @@ -1070,6 +1160,18 @@ export class AddCollectionPanel extends React.Component = ({ }, subscriptionType: SubscriptionType[subscriptionType], subscriptionQuotaId: userContext.quotaId, - defaultsCheck: { - flight: userContext.addCollectionFlight, - }, dataExplorerArea: Constants.Areas.ContextualPane, }; @@ -75,7 +72,6 @@ export const AddDatabasePanel: FunctionComponent = ({ subscriptionQuotaId: userContext.quotaId, defaultsCheck: { throughput, - flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, }; diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 0dd145af9..cd91a6628 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -59,7 +59,6 @@ export const CassandraAddCollectionPane: FunctionComponent Promise; +} + +export const ChangePartitionKeyPane: React.FC = ({ + sourceDatabase, + sourceCollection, + explorer, + onClose, +}) => { + const [targetCollectionId, setTargetCollectionId] = React.useState(); + const [createNewContainer, setCreateNewContainer] = React.useState(true); + const [formError, setFormError] = React.useState(); + const [isExecuting, setIsExecuting] = React.useState(false); + const [subPartitionKeys, setSubPartitionKeys] = React.useState([]); + const [partitionKey, setPartitionKey] = React.useState(); + + const getCollectionOptions = (): IDropdownOption[] => { + return sourceDatabase + .collections() + .filter((collection) => collection.id !== sourceCollection.id) + .map((collection) => ({ + key: collection.id(), + text: collection.id(), + })); + }; + + const submit = async () => { + if (!validateInputs()) { + return; + } + setIsExecuting(true); + try { + createNewContainer && (await createContainer()); + await createDataTransferJob(); + await onClose(); + } catch (error) { + handleError(error, "ChangePartitionKey", "Failed to start data transfer job"); + } + setIsExecuting(false); + useSidePanel.getState().closeSidePanel(); + }; + + const validateInputs = (): boolean => { + if (!createNewContainer && !targetCollectionId) { + setFormError("Choose an existing container"); + return false; + } + return true; + }; + + const createDataTransferJob = async () => { + const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`; + const dataTransferParams: DataTransferParams = { + jobName, + apiType: userContext.apiType, + subscriptionId: userContext.subscriptionId, + resourceGroupName: userContext.resourceGroup, + accountName: userContext.databaseAccount.name, + sourceDatabaseName: sourceDatabase.id(), + sourceCollectionName: sourceCollection.id(), + targetDatabaseName: sourceDatabase.id(), + targetCollectionName: targetCollectionId, + }; + await initiateDataTransfer(dataTransferParams); + }; + + const createContainer = async () => { + const partitionKeyString = partitionKey.trim(); + const partitionKeyData: DataModels.PartitionKey = partitionKeyString + ? { + paths: [partitionKeyString, ...(subPartitionKeys.length > 0 ? subPartitionKeys : [])], + kind: subPartitionKeys.length > 0 ? "MultiHash" : "Hash", + version: 2, + } + : undefined; + + const createCollectionParams: DataModels.CreateCollectionParams = { + createNewDatabase: false, + collectionId: targetCollectionId, + databaseId: sourceDatabase.id(), + databaseLevelThroughput: isSelectedDatabaseSharedThroughput(), + offerThroughput: sourceCollection.offer()?.manualThroughput, + autoPilotMaxThroughput: sourceCollection.offer()?.autoscaleMaxThroughput, + partitionKey: partitionKeyData, + }; + await createCollection(createCollectionParams); + await explorer.refreshAllDatabases(); + }; + + const isSelectedDatabaseSharedThroughput = (): boolean => { + const selectedDatabase = useDatabases + .getState() + .databases?.find((database) => database.id() === sourceDatabase.id()); + return !!selectedDatabase?.offer(); + }; + + return ( + + + + When changing a container’s partition key, you will need to create a destination container with the correct + partition key. You may also select an existing destination container.  + + Learn more + + + + + + + Database id + + + + + + + + +
+ setCreateNewContainer(true)} + /> + New container + + setCreateNewContainer(false)} + /> + Existing container +
+
+ {createNewContainer ? ( + + All configurations except for unique keys will be copied from the source container + + + + + {`${getCollectionName()} id`} + + + + + + ) => setTargetCollectionId(event.target.value)} + /> + + + + + + {getPartitionKeyName(userContext.apiType)} + + + + + + + + {getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)} + + + ) => { + if (!partitionKey && !event.target.value.startsWith("/")) { + setPartitionKey("/" + event.target.value); + } else { + setPartitionKey(event.target.value); + } + }} + /> + {subPartitionKeys.map((subPartitionKey: string, index: number) => { + return ( + +
+ 0 ? 1 : 0} + className="panelTextField" + autoComplete="off" + placeholder={getPartitionKeyPlaceHolder(userContext.apiType, index)} + aria-label={getPartitionKeyName(userContext.apiType)} + pattern={".*"} + title={""} + value={subPartitionKey} + onChange={(event: React.ChangeEvent) => { + const keys = [...subPartitionKeys]; + if (!keys[index] && !event.target.value.startsWith("/")) { + keys[index] = "/" + event.target.value.trim(); + setSubPartitionKeys(keys); + } else { + keys[index] = event.target.value.trim(); + setSubPartitionKeys(keys); + } + }} + /> + { + const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j); + setSubPartitionKeys(keys); + }} + /> +
+ ); + })} + + = Constants.BackendDefaults.maxNumMultiHashPartition} + onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} + > + Add hierarchical partition key + + {subPartitionKeys.length > 0 && ( + + This feature allows you to + partition your data with up to three levels of keys for better data distribution. Requires .NET V3, + Java V4 SDK, or preview JavaScript V3 SDK.{" "} + + Learn more + + + )} + +
+
+ ) : ( + + + + + {`${getCollectionName()}`} + + + + + + + , collection: IDropdownOption) => { + setTargetCollectionId(collection.key as string); + setFormError(""); + }} + defaultSelectedKey={targetCollectionId} + responsiveMode={999} + /> + + )} +
+
+ ); +}; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index e63cb45bd..8add09097 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -2140,6 +2140,11 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
+ `; diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap index 6e0cbbeab..6c5cb5dfb 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap @@ -7073,6 +7073,11 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4a3a8942e..19f98d5c6 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` Object { "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Panes/PanelComponent.less b/src/Explorer/Panes/PanelComponent.less index 481c6ee64..3a0ab9b3c 100644 --- a/src/Explorer/Panes/PanelComponent.less +++ b/src/Explorer/Panes/PanelComponent.less @@ -48,6 +48,13 @@ font-size: @mediumFontSize; padding: 0 @LargeSpace 0 @SmallSpace; } + + .panelSectionSpinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } } } diff --git a/src/Explorer/Panes/PanelContainerComponent.tsx b/src/Explorer/Panes/PanelContainerComponent.tsx index fa2db4f42..bad1719be 100644 --- a/src/Explorer/Panes/PanelContainerComponent.tsx +++ b/src/Explorer/Panes/PanelContainerComponent.tsx @@ -58,7 +58,7 @@ export class PanelContainerComponent extends React.Component = ({ const handleOnSubmit = (event: React.FormEvent) => { event.preventDefault(); onSubmit(); + const screenReaderStatusElement = document.getElementById("screenReaderStatus"); + if (screenReaderStatusElement) { + screenReaderStatusElement.innerHTML = labelToLoadingItemName[submitButtonText] || "Loading"; + } }; return ( @@ -42,6 +47,7 @@ export const RightPaneForm: FunctionComponent = ({ /> )} + {isExecuting && } ); diff --git a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap index bcfa8a9e6..7691f6ed4 100644 --- a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap +++ b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap @@ -1782,5 +1782,10 @@ exports[`Right Pane Form should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index ef25a7d73..9b2412c4b 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -11,7 +11,13 @@ import { import * as Constants from "Common/Constants"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { configContext } from "ConfigContext"; -import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { + DefaultRUThreshold, + LocalStorageUtility, + StorageKey, + getRUThreshold, + ruThresholdEnabled as isRUThresholdEnabled, +} from "Shared/StorageUtility"; import * as StringUtility from "Shared/StringUtility"; import { userContext } from "UserContext"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; @@ -35,6 +41,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ? Constants.Queries.UnlimitedPageOption : Constants.Queries.CustomPageOption, ); + const [ruThresholdEnabled, setRUThresholdEnabled] = useState(isRUThresholdEnabled()); + const [ruThreshold, setRUThreshold] = useState(getRUThreshold()); const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState( LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), ); @@ -103,6 +111,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage, ); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); + LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval); @@ -120,6 +129,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ); } + if (ruThresholdEnabled) { + LocalStorageUtility.setEntryNumber(StorageKey.RUThreshold, ruThreshold); + } + if (queryTimeoutEnabled) { LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout); LocalStorageUtility.setEntryBoolean( @@ -195,6 +208,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setPageOption(option.key); }; + const handleOnRUThresholdToggleChange = (ev: React.MouseEvent, checked?: boolean): void => { + setRUThresholdEnabled(checked); + }; + + const handleOnRUThresholdSpinButtonChange = (ev: React.MouseEvent, newValue?: string): void => { + const ruThreshold = Number(newValue); + if (!isNaN(ruThreshold)) { + setRUThreshold(ruThreshold); + } + }; + const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent, checked?: boolean): void => { setQueryTimeoutEnabled(checked); }; @@ -234,7 +258,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ const handleSampleDatabaseChange = async (ev: React.MouseEvent, checked?: boolean): Promise => { setCopilotSampleDBEnabled(checked); useQueryCopilot.getState().setCopilotSampleDBEnabled(checked); - setRefreshExplorer(!refreshExplorer); + setRefreshExplorer(false); }; const choiceButtonStyles = { @@ -259,7 +283,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ], }; - const queryTimeoutToggleStyles: IToggleStyles = { + const toggleStyles: IToggleStyles = { label: { fontSize: 12, fontWeight: 400, @@ -272,7 +296,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ text: {}, }; - const queryTimeoutSpinButtonStyles: ISpinButtonStyles = { + const spinButtonStyles: ISpinButtonStyles = { label: { fontSize: 12, fontWeight: 400, @@ -338,48 +362,83 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} {userContext.apiType === "SQL" && ( -
-
-
- - Query Timeout - - - When a query reaches a specified time limit, a popup with an option to cancel the query will show - unless automatic cancellation has been enabled - -
-
- -
- {queryTimeoutEnabled && ( + <> +
+
+
+ + RU Threshold + + If a query exceeds a configured RU threshold, the query will be aborted. +
-
- )} + {ruThresholdEnabled && ( +
+ +
+ )} +
-
+
+
+
+ + Query Timeout + + + When a query reaches a specified time limit, a popup with an option to cancel the query will show + unless automatic cancellation has been enabled + +
+
+ +
+ {queryTimeoutEnabled && ( +
+ + +
+ )} +
+
+ )}
@@ -404,7 +463,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} - styles={queryTimeoutSpinButtonStyles} + styles={spinButtonStyles} />
@@ -426,7 +485,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} - styles={queryTimeoutSpinButtonStyles} + styles={spinButtonStyles} />
@@ -448,7 +507,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)} onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)} onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} - styles={queryTimeoutSpinButtonStyles} + styles={spinButtonStyles} />
@@ -571,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ Enable sample database This is a sample database and collection with synthetic product data you can use to explore using - NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is + NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is created by, and maintained by Microsoft at no cost to you.
@@ -581,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ label: { padding: 0 }, }} className="padding" - ariaLabel="Enable sample db for Copilot" + ariaLabel="Enable sample db for Query Advisor" checked={copilotSampleDBEnabled} onChange={handleSampleDatabaseChange} /> diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index f4de6deab..6f4ac0c06 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -97,6 +97,74 @@ exports[`Settings Pane should render Default properly 1`] = `
+
+
+
+ + RU Threshold + + + If a query exceeds a configured RU threshold, the query will be aborted. + +
+
+ +
+
+ +
+
+
diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 8054abe19..e8553fa4e 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -2450,6 +2449,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
+ `; diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index c9d9bda10..7d73ccc1f 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -1,5 +1,6 @@ import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; +import { logConsoleError } from "Utils/NotificationConsoleUtils"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as _ from "underscore"; import AddPropertyIcon from "../../../../images/Add-property.svg"; @@ -97,9 +98,19 @@ export const AddTableEntityPanel: FunctionComponent = /* Add new entity attribute */ const onSubmit = async (): Promise => { for (let i = 0; i < entities.length; i++) { - const { property, type } = entities[i]; - if (property === "" || property === undefined) { - setFormError(`Property name cannot be empty. Please enter a property name`); + const { property, type, value } = entities[i]; + if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) { + logConsoleError(`${property} cannot be empty. Please input a value for ${property}`); + setFormError(`${property} cannot be empty. Please input a value for ${property}`); + return; + } + + if ( + (property === "PartitionKey" && containsAnyWhiteSpace(value) === true) || + (property === "RowKey" && containsAnyWhiteSpace(value) === true) + ) { + logConsoleError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`); + setFormError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`); return; } @@ -107,12 +118,14 @@ export const AddTableEntityPanel: FunctionComponent = setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`); return; } + + setFormError(""); } setIsExecuting(true); const entity: Entities.ITableEntity = entityFromAttributes(entities); - const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); try { + const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); await tableEntityListViewModel.addEntityToCache(newEntity); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { tableEntityListViewModel.redrawTableThrottled(); @@ -127,6 +140,13 @@ export const AddTableEntityPanel: FunctionComponent = } }; + const containsAnyWhiteSpace = (entityValue: string) => { + if (/\s/.test(entityValue)) { + return true; + } + return false; + }; + const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { let newHeaders: string[] = []; const keys = Object.keys(newEntity); @@ -182,9 +202,14 @@ export const AddTableEntityPanel: FunctionComponent = const entityChange = (value: string | Date, indexOfInput: number, key: string): void => { const cloneEntities: EntityRowType[] = [...entities]; if (key === "property") { - cloneEntities[indexOfInput].property = value.toString(); + cloneEntities[indexOfInput].property = value.toString().trim(); } else if (key === "time") { cloneEntities[indexOfInput].entityTimeValue = value.toString(); + } else if ( + cloneEntities[indexOfInput].property === "PartitionKey" || + cloneEntities[indexOfInput].property === "RowKey" + ) { + cloneEntities[indexOfInput].value = value.toString().trim(); } else { cloneEntities[indexOfInput].value = value.toString(); } @@ -236,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent = { entityChange(newInput, selectedRow, "value"); diff --git a/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx b/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx index be2f982e0..e59fceee9 100644 --- a/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/EditTableEntityPanel.tsx @@ -1,5 +1,6 @@ import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; +import { logConsoleError } from "Utils/NotificationConsoleUtils"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as _ from "underscore"; import AddPropertyIcon from "../../../../images/Add-property.svg"; @@ -190,7 +191,7 @@ export const EditTableEntityPanel: FunctionComponent const onSubmit = async (): Promise => { for (let i = 0; i < entities.length; i++) { - const { property, type } = entities[i]; + const { property, type, value } = entities[i]; if (property === "" || property === undefined) { setFormError(`Property name cannot be empty. Please enter a property name`); return; @@ -200,6 +201,17 @@ export const EditTableEntityPanel: FunctionComponent setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`); return; } + + if ( + (property === "PartitionKey" && value === "") || + (property === "PartitionKey" && value === undefined) || + (property === "RowKey" && value === "") || + (property === "RowKey" && value === undefined) + ) { + logConsoleError(`${property} cannot be empty. Please input a value for ${property}`); + setFormError(`${property} cannot be empty. Please input a value for ${property}`); + return; + } } setIsExecuting(true); @@ -359,7 +371,7 @@ export const EditTableEntityPanel: FunctionComponent selectedKey={entity.type} entityPropertyPlaceHolder={detailedHelp} entityValuePlaceholder={entity.entityValuePlaceholder} - entityValue={entity.value?.toString()} + entityValue={entity.value.toString()} isEntityTypeDate={entity.isEntityTypeDate} entityTimeValue={entity.entityTimeValue} isEntityValueDisable={entity.isEntityValueDisable} diff --git a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap index ca5c6c926..4a913958d 100644 --- a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap @@ -3019,6 +3019,11 @@ exports[`Table query select Panel should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap index 80d494808..f2ee599f7 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap @@ -2130,6 +2130,11 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap index da5c0c722..648f797f7 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap @@ -2136,6 +2136,11 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx b/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx deleted file mode 100644 index d9b1d9792..000000000 --- a/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Upload } from "Common/Upload/Upload"; -import { useSidePanel } from "hooks/useSidePanel"; -import React, { ChangeEvent, FunctionComponent, useState } from "react"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils"; -import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; -import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; - -export interface UploadFilePanelProps { - uploadFile: (name: string, content: string) => Promise; -} - -export const UploadFilePane: FunctionComponent = ({ uploadFile }: UploadFilePanelProps) => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const extensions: string = undefined; //ex. ".ipynb" - const errorMessage = "Could not upload file"; - const inProgressMessage = "Uploading file to notebook server"; - const successMessage = "Successfully uploaded file to notebook server"; - - const [files, setFiles] = useState(); - const [formErrors, setFormErrors] = useState(""); - const [isExecuting, setIsExecuting] = useState(false); - - const submit = () => { - setFormErrors(""); - if (!files || files.length === 0) { - setFormErrors("No file specified. Please input a file."); - logConsoleError(`${errorMessage} -- No file specified. Please input a file.`); - return; - } - - const file: File = files.item(0); - - const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`); - - setIsExecuting(true); - - onSubmit(files.item(0)) - .then( - () => { - logConsoleInfo(`${successMessage} ${file.name}`); - closeSidePanel(); - }, - (error: string) => { - setFormErrors(errorMessage); - logConsoleError(`${errorMessage} ${file.name}: ${error}`); - }, - ) - .finally(() => { - setIsExecuting(false); - clearMessage(); - }); - }; - - const updateSelectedFiles = (event: ChangeEvent): void => { - setFiles(event.target.files); - }; - - const onSubmit = async (file: File): Promise => { - const readFileAsText = (inputFile: File): Promise => { - const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onerror = () => { - reader.abort(); - reject(`Problem parsing file: ${inputFile}`); - }; - reader.onload = () => { - resolve(reader.result as string); - }; - reader.readAsText(inputFile); - }); - }; - - const fileContent = await readFileAsText(file); - return uploadFile(file.name, fileContent); - }; - - const props: RightPaneFormProps = { - formError: formErrors, - isExecuting: isExecuting, - submitButtonText: "Upload", - onSubmit: submit, - }; - - return ( - -
- -
-
- ); -}; diff --git a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap index 0a19a654b..0831c5a10 100644 --- a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap @@ -3,6 +3,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys. diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 4ac4a9e20..6e32d62c0 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -2810,6 +2810,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
+ `; diff --git a/src/Explorer/Panes/__snapshots__/PanelContainerComponent.test.tsx.snap b/src/Explorer/Panes/__snapshots__/PanelContainerComponent.test.tsx.snap index 925e7b427..2d2672404 100644 --- a/src/Explorer/Panes/__snapshots__/PanelContainerComponent.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/PanelContainerComponent.test.tsx.snap @@ -2,7 +2,7 @@ exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = ` +
@@ -68,9 +68,14 @@ export const QueryCopilotFeedbackModal = ({ rows={3} /> diff --git a/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx b/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx index 8c22cec43..94316a0cc 100644 --- a/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx +++ b/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx @@ -50,7 +50,9 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => - Welcome to Microsoft Copilot for Azure in Cosmos DB + + Welcome to Microsoft Copilot for Azure in Cosmos DB (preview) + diff --git a/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap b/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap index dc7f4c96f..356b86977 100644 --- a/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap +++ b/src/Explorer/QueryCopilot/Modal/__snapshots__/QueryCopilotFeedbackModal.test.tsx.snap @@ -3,6 +3,14 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = ` - Welcome to Microsoft Copilot for Azure in Cosmos DB + Welcome to Microsoft Copilot for Azure in Cosmos DB (preview) set({ queryResults }), setErrorMessage: (errorMessage: string) => set({ errorMessage }), setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }), + setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }), setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }), setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }), setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }), @@ -103,6 +105,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme queryResults: undefined, errorMessage: "", isSamplePromptsOpen: false, + showPromptTeachingBubble: true, showDeletePopup: false, showFeedbackBar: false, showCopyPopup: false, diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index d3ed8b24c..6c54cd506 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -11,18 +11,16 @@ import { Link, MessageBar, MessageBarType, + ProgressIndicator, Separator, - Spinner, Stack, TeachingBubble, Text, TextField, } from "@fluentui/react"; -import { useBoolean } from "@fluentui/react-hooks"; import { HttpStatusCodes } from "Common/Constants"; import { handleError } from "Common/ErrorHandlingUtils"; import { createUri } from "Common/UrlUtility"; -import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { @@ -38,7 +36,6 @@ import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import React, { useRef, useState } from "react"; import HintIcon from "../../../images/Hint.svg"; -import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import RecentIcon from "../../../images/Recent.svg"; import errorIcon from "../../../images/close-black.svg"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -71,7 +68,7 @@ export const QueryCopilotPromptbar: React.FC = ({ databaseId, containerId, }: QueryCopilotPromptProps): JSX.Element => { - const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); + const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState(false); const inputEdited = useRef(false); const { openFeedbackModal, @@ -94,6 +91,8 @@ export const QueryCopilotPromptbar: React.FC = ({ setIsSamplePromptsOpen, showSamplePrompts, setShowSamplePrompts, + showPromptTeachingBubble, + setShowPromptTeachingBubble, showDeletePopup, setShowDeletePopup, showFeedbackBar, @@ -215,12 +214,12 @@ export const QueryCopilotPromptbar: React.FC = ({ const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); if (response.ok) { if (generateSQLQueryResponse?.sql !== "N/A") { - let query = `-- **Prompt:** ${userPrompt}\r\n`; - if (generateSQLQueryResponse.explanation) { - query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`; - } - query += generateSQLQueryResponse.sql; - setQuery(query); + const queryExplanation = `-- **Explanation of query:** ${ + generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A" + }\r\n`; + const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql; + const lastQuery = generatedQuery && query ? `${query}\r\n` : ""; + setQuery(`${lastQuery}${currentGeneratedQuery}`); setGeneratedQuery(generateSQLQueryResponse.sql); setGeneratedQueryComments(generateSQLQueryResponse.explanation); setShowFeedbackBar(true); @@ -271,19 +270,9 @@ export const QueryCopilotPromptbar: React.FC = ({ } }; - const showTeachingBubble = (): void => { - if (!inputEdited.current) { - setTimeout(() => { - if (!inputEdited.current && !isWelcomModalVisible()) { - toggleCopilotTeachingBubbleVisible(); - inputEdited.current = true; - } - }, 30000); - } - }; - - const isWelcomModalVisible = (): boolean => { - return localStorage.getItem("hideWelcomeModal") !== "true"; + const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => { + setCopilotTeachingBubbleVisible(visible); + setShowPromptTeachingBubble(visible); }; const clearFeedback = () => { @@ -314,19 +303,394 @@ export const QueryCopilotPromptbar: React.FC = ({ }; React.useEffect(() => { - showTeachingBubble(); useTabs.getState().setIsQueryErrorThrown(false); }, []); return ( - - Copilot - Copilot + + + + { + inputEdited.current = true; + setShowSamplePrompts(true); + }} + onKeyDown={(e) => { + if (e.key === "Enter" && userPrompt) { + inputEdited.current = true; + startGenerateQueryProcess(); + } + }} + style={{ lineHeight: 30 }} + styles={{ + root: { width: "100%" }, + suffix: { + background: "none", + padding: 0, + }, + fieldGroup: { + borderRadius: 4, + borderColor: "#D1D1D1", + "::after": { + border: "inherit", + borderWidth: 2, + borderBottomColor: "#464FEB", + borderRadius: 4, + }, + }, + }} + disabled={isGeneratingQuery} + autoComplete="off" + placeholder="Ask a question in natural language and we’ll generate the query for you." + aria-labelledby="copilot-textfield-label" + onRenderSuffix={() => { + return ( + startGenerateQueryProcess()} + aria-label="Send" + /> + ); + }} + /> + {showPromptTeachingBubble && copilotTeachingBubbleVisible && ( + toggleCopilotTeachingBubbleVisible(false)} + hasSmallHeadline={true} + headline="Write a prompt" + > + Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "} + { + setShowSamplePrompts(true); + toggleCopilotTeachingBubbleVisible(false); + }} + style={{ color: "white", fontWeight: 600 }} + > + sample prompts + {" "} + or write your own query + + )} + {showSamplePrompts && ( + setShowSamplePrompts(false)} + directionalHintFixed={true} + directionalHint={DirectionalHint.bottomLeftEdge} + alignTargetEdge={true} + gapSpace={4} + > + + {filteredHistories?.length > 0 && ( + + + Recent + + {filteredHistories.map((history, i) => ( + { + setUserPrompt(history); + setShowSamplePrompts(false); + inputEdited.current = true; + }} + onRenderIcon={() => } + styles={promptStyles} + > + {history} + + ))} + + )} + {filteredSuggestedPrompts?.length > 0 && ( + + + Suggested Prompts + + {filteredSuggestedPrompts.map((prompt) => ( + { + setUserPrompt(prompt.text); + setShowSamplePrompts(false); + inputEdited.current = true; + }} + onRenderIcon={() => } + styles={promptStyles} + > + {prompt.text} + + ))} + + )} + {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( + + + + Learn about{" "} + + writing effective prompts + + + + )} + + + )} + + {!isGeneratingQuery && ( + + {!showFeedbackBar && ( + + AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} + + Read preview terms + + {showErrorMessageBar && ( + + {errorMessage ? errorMessage : "We ran into an error and were not able to execute query."} + + )} + {showInvalidQueryMessageBar && ( + + We were unable to generate a query based upon the prompt provided. Please modify the prompt and + try again. For examples of how to write a good prompt, please read + + this article. + {" "} + Our content guidelines can be found + + here. + + + )} + + )} + {showFeedbackBar && ( + + {userContext.feedbackPolicies?.policyAllowFeedback && ( + + Provide feedback + {showCallout && !hideFeedbackModalForLikedQueries && ( + { + setShowCallout(false); + SubmitFeedback({ + params: { + generatedQuery: generatedQuery, + likeQuery: likeQuery, + description: "", + userPrompt: userPrompt, + }, + explorer, + databaseId, + containerId, + mode: isSampleCopilotActive ? "Sample" : "User", + }); + }} + directionalHint={DirectionalHint.topCenter} + > + + Thank you. Need to give{" "} + { + setShowCallout(false); + openFeedbackModal(generatedQuery, true, userPrompt); + }} + > + more feedback? + + + + )} + { + setShowCallout(!likeQuery); + setLikeQuery(!likeQuery); + if (likeQuery === true) { + document.getElementById("likeStatus").innerHTML = "Unpressed"; + } + if (likeQuery === false) { + document.getElementById("likeStatus").innerHTML = "Liked"; + } + if (dislikeQuery) { + setDislikeQuery(!dislikeQuery); + } + }} + /> + { + let toggleStatusValue = "Unpressed"; + if (!dislikeQuery) { + openFeedbackModal(generatedQuery, false, userPrompt); + setLikeQuery(false); + toggleStatusValue = "Disliked"; + } + setDislikeQuery(!dislikeQuery); + setShowCallout(false); + document.getElementById("likeStatus").innerHTML = toggleStatusValue; + }} + /> + + + + )} + + Copy code + + { + setShowDeletePopup(true); + }} + iconProps={{ iconName: "Delete" }} + style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }} + styles={{ + root: { + backgroundColor: "inherit", + }, + }} + > + Clear editor + + + )} + + )} + {isGeneratingQuery && ( + + )} + { @@ -334,284 +698,10 @@ export const QueryCopilotPromptbar: React.FC = ({ clearFeedback(); resetMessageStates(); }} - styles={{ - root: { - marginLeft: "auto !important", - }, - }} ariaLabel="Close" + title="Close copilot" /> - - { - inputEdited.current = true; - setShowSamplePrompts(true); - }} - onKeyDown={(e) => { - if (e.key === "Enter" && userPrompt) { - inputEdited.current = true; - startGenerateQueryProcess(); - } - }} - style={{ lineHeight: 30 }} - styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }} - disabled={isGeneratingQuery} - autoComplete="off" - placeholder="Ask a question in natural language and we’ll generate the query for you." - aria-labelledby="copilot-textfield-label" - /> - {copilotTeachingBubbleVisible && ( - - Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} - { - setShowSamplePrompts(true); - toggleCopilotTeachingBubbleVisible(); - }} - style={{ color: "white", fontWeight: 600 }} - > - sample prompts - {" "} - or write your own query - - )} - startGenerateQueryProcess()} - aria-label="Send" - /> -
- {isGeneratingQuery && } -
- {showSamplePrompts && ( - setShowSamplePrompts(false)} - directionalHintFixed={true} - directionalHint={DirectionalHint.bottomLeftEdge} - alignTargetEdge={true} - gapSpace={4} - > - - {filteredHistories?.length > 0 && ( - - - Recent - - {filteredHistories.map((history, i) => ( - { - setUserPrompt(history); - setShowSamplePrompts(false); - inputEdited.current = true; - }} - onRenderIcon={() => } - styles={promptStyles} - > - {history} - - ))} - - )} - {filteredSuggestedPrompts?.length > 0 && ( - - - Suggested Prompts - - {filteredSuggestedPrompts.map((prompt) => ( - { - setUserPrompt(prompt.text); - setShowSamplePrompts(false); - inputEdited.current = true; - }} - onRenderIcon={() => } - styles={promptStyles} - > - {prompt.text} - - ))} - - )} - {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( - - - - Learn about{" "} - - writing effective prompts - - - - )} - - - )} -
- - - - AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} - - Read preview terms - - {showErrorMessageBar && ( - - {errorMessage ? errorMessage : "We ran into an error and were not able to execute query."} - - )} - {showInvalidQueryMessageBar && ( - - We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again. - For examples of how to write a good prompt, please read - - this article. - {" "} - Our content guidelines can be found - - here. - - - )} - - - - {showFeedbackBar && ( - - Provide feedback on the query generated - {showCallout && !hideFeedbackModalForLikedQueries && ( - { - setShowCallout(false); - SubmitFeedback({ - params: { - generatedQuery: generatedQuery, - likeQuery: likeQuery, - description: "", - userPrompt: userPrompt, - }, - explorer, - databaseId, - containerId, - mode: isSampleCopilotActive ? "Sample" : "User", - }); - }} - directionalHint={DirectionalHint.topCenter} - > - - Thank you. Need to give{" "} - { - setShowCallout(false); - openFeedbackModal(generatedQuery, true, userPrompt); - }} - > - more feedback? - - - - )} - { - setShowCallout(!likeQuery); - setLikeQuery(!likeQuery); - if (dislikeQuery) { - setDislikeQuery(!dislikeQuery); - } - }} - /> - { - if (!dislikeQuery) { - openFeedbackModal(generatedQuery, false, userPrompt); - setLikeQuery(false); - } - setDislikeQuery(!dislikeQuery); - setShowCallout(false); - }} - aria-label="Dislike" - /> - - - Copy query - - { - setShowDeletePopup(true); - }} - iconProps={{ iconName: "Delete" }} - style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} - > - Delete query - - - )} - {isSamplePromptsOpen && } {query !== "" && query.trim().length !== 0 && ( = ({ explorer }: Query const executeQueryBtn = { iconSrc: ExecuteQueryIcon, iconAlt: executeQueryBtnLabel, - onCommandClick: () => OnExecuteQueryClick(useQueryCopilot), + onCommandClick: () => OnExecuteQueryClick(useQueryCopilot as Partial), commandButtonLabel: executeQueryBtnLabel, ariaLabel: executeQueryBtnLabel, hasPopup: false, @@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC = ({ explorer }: Query const toggleCopilotButton = { iconSrc: QueryCommandIcon, - iconAlt: "Copilot", + iconAlt: "Query Advisor", onCommandClick: () => { toggleCopilot(true); }, - commandButtonLabel: "Copilot", - ariaLabel: "Copilot", + commandButtonLabel: "Query Advisor", + ariaLabel: "Query Advisor", hasPopup: false, disabled: copilotActive, }; diff --git a/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts b/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts index 6d2cafdc5..59101cbb3 100644 --- a/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts +++ b/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts @@ -1,6 +1,7 @@ import { FeedOptions } from "@azure/cosmos"; import { Areas, + BackendApi, ConnectionStatusType, ContainerStatusType, HttpStatusCodes, @@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { userContext } from "UserContext"; import { getAuthorizationHeader } from "Utils/AuthorizationUtils"; +import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { useTabs } from "hooks/useTabs"; @@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis }; export const getCopilotEnabled = async (): Promise => { - const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`; + const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings) + ? configContext.PORTAL_BACKEND_ENDPOINT + : configContext.BACKEND_ENDPOINT; + + const url = `${backendEndpoint}/api/portalsettings/querycopilot`; const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const headers = { [authorizationHeader.header]: authorizationHeader.token }; diff --git a/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx b/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx index 8d872e174..3b552cfa8 100644 --- a/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx +++ b/src/Explorer/QueryCopilot/Shared/QueryCopilotResults.tsx @@ -1,6 +1,6 @@ import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; -import { useQueryCopilot } from "hooks/useQueryCopilot"; +import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import React from "react"; export const QueryCopilotResults: React.FC = (): JSX.Element => { @@ -12,7 +12,11 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => { queryResults={useQueryCopilot.getState().queryResults} isExecuting={useQueryCopilot.getState().isExecuting} executeQueryDocumentsPage={(firstItemIndex: number) => - QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot) + QueryDocumentsPerPage( + firstItemIndex, + useQueryCopilot.getState().queryIterator, + useQueryCopilot as Partial, + ) } /> ); diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 26b52ff90..bb942bf55 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index ce5385329..0ed9351ae 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,7 +25,6 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; -import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; @@ -148,12 +147,12 @@ export class SplashScreen extends React.Component { />
- {useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && ( + {useQueryCopilot.getState().copilotEnabled && ( { const copilotVersion = userContext.features.copilotVersion; @@ -410,14 +409,6 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); - } else if (useNotebook.getState().isPhoenixNotebooks) { - const newNotebookBtn = { - iconSrc: NotebookColorIcon, - title: "New notebook", - description: "Visualize your data stored in Azure Cosmos DB", - onClick: () => this.container.onNewNotebookClicked(), - }; - heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); @@ -689,11 +680,20 @@ export class SplashScreen extends React.Component { title: "Learn the Fundamentals", description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.", }; - let items: item[]; + + const commonItems: item[] = [ + { + link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts", + title: "Data Explorer keyboard shortcuts", + description: "Learn keyboard shortcuts to navigate Data Explorer.", + }, + ]; + + let apiItems: item[]; switch (userContext.apiType) { case "SQL": case "Postgres": - items = [ + apiItems = [ { link: "https://aka.ms/msl-sdk-connect", title: "Get Started using an SDK", @@ -708,7 +708,7 @@ export class SplashScreen extends React.Component { ]; break; case "Mongo": - items = [ + apiItems = [ { link: "https://aka.ms/mongonodejs", title: "Build an app with Node.js", @@ -723,7 +723,7 @@ export class SplashScreen extends React.Component { ]; break; case "Cassandra": - items = [ + apiItems = [ { link: "https://aka.ms/cassandracontainer", title: "Create a Container", @@ -738,7 +738,7 @@ export class SplashScreen extends React.Component { ]; break; case "Gremlin": - items = [ + apiItems = [ { link: "https://aka.ms/graphquickstart", title: "Get Started ", @@ -753,7 +753,7 @@ export class SplashScreen extends React.Component { ]; break; case "Tables": - items = [ + apiItems = [ { link: "https://aka.ms/tabledotnet", title: "Build a .NET App", @@ -770,6 +770,9 @@ export class SplashScreen extends React.Component { default: break; } + + const items = [...commonItems, ...apiItems]; + return ( {items.map((item, i) => ( diff --git a/src/Explorer/Tables/Constants.ts b/src/Explorer/Tables/Constants.ts index 76c050f5d..58d5b8e87 100644 --- a/src/Explorer/Tables/Constants.ts +++ b/src/Explorer/Tables/Constants.ts @@ -225,3 +225,8 @@ export const InputType = { DateTime: "datetime-local", Number: "number", }; + +export const labelToLoadingItemName: Record = { + "Add Row": "Adding row to table", + "Add Entity": "Adding entity to table", +}; diff --git a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts index 6616a3af5..3ef0037aa 100644 --- a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts +++ b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts @@ -42,6 +42,11 @@ function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewM createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start. $(window).resize(updateTableScrollableRegionMetrics); + operationManager.focusTable(); // Also selects the first row if needed. + // Attach the arrow key event handler to the table element + $dataTable.on("keydown", (event: JQueryEventObject) => { + handleArrowKey(element, valueAccessor, allBindings, viewModel, bindingContext, event); + }); } function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) { @@ -210,6 +215,39 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi }); //selected = bindingContext.$data.selected(); } +function handleArrowKey( + element: any, + valueAccessor: any, + allBindings: any, + viewModel: any, + bindingContext: any, + event: JQueryEventObject, +) { + const isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow; + const isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow; + + if (isUpArrowKey || isDownArrowKey) { + const $dataTable = $(element); + let $selectedRow = $dataTable.find("tr.selected"); + + if ($selectedRow.length === 0) { + // No row is currently selected, select the first row + $selectedRow = $dataTable.find("tr:first"); + $selectedRow.addClass("selected"); + } else { + const $targetRow = isUpArrowKey ? $selectedRow.prev("tr") : $selectedRow.next("tr"); + + if ($targetRow.length > 0) { + // Remove the selected class from the current row and add it to the target row + $selectedRow.removeClass("selected").attr("tabindex", "-1"); + $targetRow.addClass("selected").attr("tabindex", "0"); + $targetRow.focus(); + } + } + + event.preventDefault(); + } +} function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) { // do nothing for now diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 830a2544f..a9d7c96a6 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -3,6 +3,7 @@ import * as ko from "knockout"; import Q from "q"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; +import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; import { handleError } from "../../Common/ErrorHandlingUtils"; import * as HeadersUtility from "../../Common/HeadersUtility"; import { createDocument } from "../../Common/dataAccess/createDocument"; @@ -171,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(entity); }, (error) => { - handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -261,6 +263,57 @@ export class CassandraAPIDataClient extends TableDataClient { query: string, shouldNotify?: boolean, paginationToken?: string, + ): Promise { + if (!this.useCassandraProxyEndpoint("postQuery")) { + return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken); + } + const clearMessage = + shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); + try { + const { authType, databaseAccount } = userContext; + + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? CassandraProxyAPIs.connectionStringQueryApi + : CassandraProxyAPIs.queryApi; + + const data: any = await $.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint), + resourceId: databaseAccount?.id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + query, + paginationToken, + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }); + shouldNotify && + NotificationConsoleUtils.logConsoleInfo( + `Successfully fetched ${data.result.length} rows for table ${collection.id()}`, + ); + return { + Results: data.result, + ContinuationToken: data.paginationToken, + }; + } catch (error) { + shouldNotify && + handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + throw error; + } finally { + clearMessage?.(); + } + } + + public async queryDocuments_ToBeDeprecated( + collection: ViewModels.Collection, + query: string, + shouldNotify?: boolean, + paginationToken?: string, ): Promise { const clearMessage = shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); @@ -294,7 +347,11 @@ export class CassandraAPIDataClient extends TableDataClient { }; } catch (error) { shouldNotify && - handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); + handleError( + error, + "QueryDocuments_ToBeDeprecated_Cassandra", + `Failed to query rows for table ${collection.id()}`, + ); throw error; } finally { clearMessage?.(); @@ -350,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(); }, (error) => { + const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError( - error, + errorText, "CreateKeyspaceCassandra", `Error while creating a keyspace with query ${createKeyspaceQuery}`, ); - deferred.reject(error); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -388,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(); }, (error) => { - handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError( + errorText, + "CreateTableCassandra", + `Error while creating a table with query ${createTableQuery}`, + ); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -402,6 +465,51 @@ export class CassandraAPIDataClient extends TableDataClient { } public getTableKeys(collection: ViewModels.Collection): Q.Promise { + if (!this.useCassandraProxyEndpoint("getKeys")) { + return this.getTableKeys_ToBeDeprecated(collection); + } + + if (!!collection.cassandraKeys) { + return Q.resolve(collection.cassandraKeys); + } + const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`); + const { authType, databaseAccount } = userContext; + const apiEndpoint: string = + authType === AuthType.EncryptedToken ? CassandraProxyAPIs.connectionStringKeysApi : CassandraProxyAPIs.keysApi; + + let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`; + const deferred = Q.defer(); + + $.ajax(endpoint, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint), + resourceId: databaseAccount?.id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }) + .then( + (data: CassandraTableKeys) => { + collection.cassandraKeys = data; + logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`); + deferred.resolve(data); + }, + (error: any) => { + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(errorText); + }, + ) + .done(clearInProgressMessage); + return deferred.promise; + } + + public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise { if (!!collection.cassandraKeys) { return Q.resolve(collection.cassandraKeys); } @@ -433,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data); }, (error: any) => { - handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -442,6 +551,52 @@ export class CassandraAPIDataClient extends TableDataClient { } public getTableSchema(collection: ViewModels.Collection): Q.Promise { + if (!this.useCassandraProxyEndpoint("getSchema")) { + return this.getTableSchema_ToBeDeprecated(collection); + } + + if (!!collection.cassandraSchema) { + return Q.resolve(collection.cassandraSchema); + } + const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`); + const { databaseAccount, authType } = userContext; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? CassandraProxyAPIs.connectionStringSchemaApi + : CassandraProxyAPIs.schemaApi; + let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`; + const deferred = Q.defer(); + + $.ajax(endpoint, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint), + resourceId: databaseAccount?.id, + keyspaceId: collection.databaseId, + tableId: collection.id(), + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }) + .then( + (data: any) => { + collection.cassandraSchema = data.columns; + logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`); + deferred.resolve(data.columns); + }, + (error: any) => { + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(errorText); + }, + ) + .done(clearInProgressMessage); + return deferred.promise; + } + + public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise { if (!!collection.cassandraSchema) { return Q.resolve(collection.cassandraSchema); } @@ -473,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data.columns); }, (error: any) => { - handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -482,6 +638,44 @@ export class CassandraAPIDataClient extends TableDataClient { } private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise { + if (!this.useCassandraProxyEndpoint("createOrDelete")) { + return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query); + } + + const deferred = Q.defer(); + const { authType, databaseAccount } = userContext; + const apiEndpoint: string = + authType === AuthType.EncryptedToken + ? CassandraProxyAPIs.connectionStringCreateOrDeleteApi + : CassandraProxyAPIs.createOrDeleteApi; + + $.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, { + type: "POST", + contentType: Constants.ContentType.applicationJson, + data: JSON.stringify({ + accountName: databaseAccount?.name, + cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint), + resourceId: resourceId, + query: query, + }), + beforeSend: this.setAuthorizationHeader as any, + cache: false, + }).then( + (data: any) => { + deferred.resolve(); + }, + (reason) => { + deferred.reject(reason); + }, + ); + return deferred.promise; + } + + private createOrDeleteQuery_ToBeDeprecated( + cassandraEndpoint: string, + resourceId: string, + query: string, + ): Q.Promise { const deferred = Q.defer(); const { authType, databaseAccount } = userContext; const apiEndpoint: string = @@ -547,4 +741,25 @@ export class CassandraAPIDataClient extends TableDataClient { private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { return collection.cassandraKeys.partitionKeys[0].property; } + + private useCassandraProxyEndpoint(api: string): boolean { + const activeCassandraProxyEndpoints: string[] = [ + CassandraProxyEndpoints.Development, + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + ]; + let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if ( + configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { + canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + + return ( + canAccessCassandraProxy && + configContext.NEW_CASSANDRA_APIS?.includes(api) && + activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT) + ); + } } diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index 4283a661c..cfe1b2039 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -80,7 +80,8 @@ placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.' }, css: { placeholderVisible: filterContent().length === 0 }, - textInput: filterContent" + textInput: filterContent, + event: { keydown: onFilterKeyDown }" /> { this.selectedDocumentId(null); - const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); + const newDocument: any = { + id: "replace_with_new_document_id", + }; + this.partitionKeyProperties.forEach((partitionKeyProperty) => { + let target = newDocument; + const keySegments = partitionKeyProperty.split("."); + const finalSegment = keySegments.pop(); + + // Initialize nested objects as needed + keySegments.forEach((segment) => { + target = target[segment] = target[segment] || {}; + }); + + target[finalSegment] = "replace_with_new_partition_key_value"; + }); + const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4); this.initialDocumentContent(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); @@ -648,9 +667,38 @@ export default class DocumentsTab extends TabsBase { this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); } + public onFilterKeyDown(model: unknown, e: KeyboardEvent): boolean { + if (e.key === "Enter") { + this.refreshDocumentsGrid(true); + + // Suppress the default behavior of the key + return false; + } else if (e.key === "Escape") { + this.onHideFilterClick(); + + // Suppress the default behavior of the key + return false; + } else { + // Allow the default behavior of the key + return true; + } + } + public async onActivate(): Promise { super.onActivate(); + this.setKeyboardActions({ + [KeyboardAction.SEARCH]: () => { + this.onShowFilterClick(); + return true; + }, + [KeyboardAction.CLEAR_SEARCH]: () => { + this.filterContent(""); + this.refreshDocumentsGrid(true); + return true; + }, + }); + if (!this._documentsIterator) { try { await this.autoPopulateContent(); @@ -881,12 +929,18 @@ export default class DocumentsTab extends TabsBase { } protected getTabsButtons(): CommandButtonComponentProps[] { + if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { + // All the following buttons require write access + return []; + } + const buttons: CommandButtonComponentProps[] = []; const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; if (this.newDocumentButton.visible()) { buttons.push({ iconSrc: NewDocumentIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_ITEM, onCommandClick: this.onNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -901,6 +955,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -915,6 +970,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onRevertNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -930,6 +986,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveExistingDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -944,6 +1001,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onRevertExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -959,6 +1017,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DeleteDocumentIcon, iconAlt: label, + keyboardAction: KeyboardAction.DELETE_ITEM, onCommandClick: this.onDeleteExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index cb47a070d..052c81ce6 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -1,3 +1,4 @@ +import { useMongoProxyEndpoint } from "Common/MongoProxyClient"; import React, { Component } from "react"; import * as Constants from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; @@ -9,7 +10,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; import TabsBase from "../TabsBase"; -import { getMongoShellOrigin } from "./getMongoShellOrigin"; import { getMongoShellUrl } from "./getMongoShellUrl"; //eslint-disable-next-line @@ -50,13 +50,15 @@ export default class MongoShellTabComponent extends Component< IMongoShellTabComponentStates > { private _logTraces: Map; + private _useMongoProxyEndpoint: boolean; constructor(props: IMongoShellTabComponentProps) { super(props); this._logTraces = new Map(); + this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); this.state = { - url: getMongoShellUrl(), + url: getMongoShellUrl(this._useMongoProxyEndpoint), }; props.onMongoShellTabAccessor({ @@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component< ) + Constants.MongoDBAccounts.defaultPort.toString(); const databaseId = this.props.collection.databaseId; const collectionId = this.props.collection.id(); - const apiEndpoint = configContext.BACKEND_ENDPOINT; + const apiEndpoint = this._useMongoProxyEndpoint + ? configContext.MONGO_PROXY_ENDPOINT + : configContext.BACKEND_ENDPOINT; const encryptedAuthToken: string = userContext.accessToken; - const targetOrigin = getMongoShellOrigin(); shellIframe.contentWindow.postMessage( { @@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component< apiEndpoint: apiEndpoint, }, }, - targetOrigin, + window.origin, ); } diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts deleted file mode 100644 index 8f62b2a0c..000000000 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { extractFeatures } from "Platform/Hosted/extractFeatures"; -import { configContext } from "../../../ConfigContext"; -import { updateUserContext } from "../../../UserContext"; -import { getMongoShellOrigin } from "./getMongoShellOrigin"; - -describe("getMongoShellOrigin", () => { - (window as { origin: string }).origin = "window_origin"; - - beforeEach(() => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "false", - "feature.enableLegacyMongoShellV2": "false", - "feature.enableLegacyMongoShellV1Debug": "false", - "feature.enableLegacyMongoShellV2Debug": "false", - "feature.loadLegacyMongoShellFromBE": "false", - }), - ), - }); - }); - - it("should return by default", () => { - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV1", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV2===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1Debug": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2Debug": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.loadLegacyMongoShellFromBE": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT); - }); -}); diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts deleted file mode 100644 index 774a4443c..000000000 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { configContext } from "../../../ConfigContext"; -import { userContext } from "../../../UserContext"; - -export function getMongoShellOrigin(): string { - if (userContext.features.loadLegacyMongoShellFromBE === true) { - return configContext.BACKEND_ENDPOINT; - } - - return window.origin; -} diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts index 1d75f682d..8b16816ab 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts @@ -1,7 +1,6 @@ -import { extractFeatures } from "Platform/Hosted/extractFeatures"; -import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; +import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; import { updateUserContext, userContext } from "../../../UserContext"; -import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl"; +import { getMongoShellUrl } from "./getMongoShellUrl"; const mongoBackendEndpoint = "https://localhost:1234"; @@ -32,175 +31,18 @@ describe("getMongoShellUrl", () => { cassandraEndpoint: "fakeCassandraEndpoint", }, }, - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "false", - "feature.enableLegacyMongoShellV2": "false", - "feature.enableLegacyMongoShellV1Debug": "false", - "feature.enableLegacyMongoShellV2Debug": "false", - "feature.loadLegacyMongoShellFromBE": "false", - }), - ), portalEnv: "prod", }); queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`; }); - it("should return /mongoshell/indexv2.html by default", () => { - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); + it("should return /indexv2.html by default", () => { + expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`); }); - it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => { - updateUserContext({ - portalEnv: "localhost", - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1Debug": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2Debug": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`); - }); - - describe("loadLegacyMongoShellFromBE===true", () => { - beforeEach(() => { - resetConfigContext(); - updateConfigContext({ - BACKEND_ENDPOINT: mongoBackendEndpoint, - platform: Platform.Hosted, - }); - - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.loadLegacyMongoShellFromBE": "true", - }), - ), - }); - }); - - it("should return /mongoshell/index.html", () => { - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - updateConfigContext({ - platform: Platform.Portal, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Portal, - BACKEND_ENDPOINT: mongoBackendEndpoint, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Hosted, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Portal, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - }); -}); - -describe("getExtensionEndpoint", () => { - it("when platform === Platform.Hosted, backendEndpoint is undefined", () => { - expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint === ''", () => { - expect(getExtensionEndpoint(Platform.Hosted, "")).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint === null", () => { - expect(getExtensionEndpoint(Platform.Hosted, null)).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint != ''", () => { - expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo"); - }); - - it("when platform === Platform.Portal, backendEndpoint is udefined", () => { - expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe(""); - }); - - it("when platform === Platform.Portal, backendEndpoint === ''", () => { - expect(getExtensionEndpoint(Platform.Portal, "")).toBe(""); - }); - - it("when platform === Platform.Portal, backendEndpoint === null", () => { - expect(getExtensionEndpoint(Platform.Portal, null)).toBe(""); - }); - - it("when platform !== Platform.Portal, backendEndpoint != ''", () => { - expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo"); + it("should return /index.html when useMongoProxyEndpoint is true", () => { + const useMongoProxyEndpoint: boolean = true; + expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`); }); }); diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index a029fe440..a3b49b373 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -1,45 +1,11 @@ -import { configContext, Platform } from "../../../ConfigContext"; import { userContext } from "../../../UserContext"; -export function getMongoShellUrl(): string { +export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string { const { databaseAccount: account } = userContext; const resourceId = account?.id; const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; - if (userContext.features.enableLegacyMongoShellV1 === true) { - return `/mongoshell/index.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV1Debug === true) { - return `/mongoshell/debug/index.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV2 === true) { - return `/mongoshell/indexv2.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV2Debug === true) { - return `/mongoshell/debug/indexv2.html?${queryString}`; - } - - if (userContext.portalEnv === "localhost") { - return `/mongoshell/indexv2.html?${queryString}`; - } - - if (userContext.features.loadLegacyMongoShellFromBE === true) { - const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`; - } - - return `/mongoshell/indexv2.html?${queryString}`; -} - -export function getExtensionEndpoint(platform: string, backendEndpoint: string): string { - const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : ""; - - const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || ""; - - return extensionEndpoint; + return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`; } diff --git a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx index 1872a8750..30d2ed9c1 100644 --- a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx @@ -3,13 +3,13 @@ import { DetailsListLayoutMode, IColumn, Icon, + IconButton, Link, Pivot, PivotItem, SelectionMode, Stack, Text, - IconButton, TooltipHost, } from "@fluentui/react"; import { HttpHeaders, NormalizedEventKey } from "Common/Constants"; @@ -18,15 +18,15 @@ import { QueryMetrics } from "Contracts/DataModels"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent"; import { userContext } from "UserContext"; +import copy from "clipboard-copy"; import { useNotificationConsole } from "hooks/useNotificationConsole"; import React from "react"; +import CopilotCopy from "../../../../images/CopilotCopy.svg"; import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg"; import QueryEditorNext from "../../../../images/Query-Editor-Next.svg"; import RunQuery from "../../../../images/RunQuery.png"; import InfoColor from "../../../../images/info_color.svg"; import { QueryResults } from "../../../Contracts/ViewModels"; -import copy from "clipboard-copy"; -import CopilotCopy from "../../../../images/CopilotCopy.svg"; interface QueryResultProps { isMongoDB: boolean; @@ -62,9 +62,12 @@ export const QueryResultSection: React.FC = ({ const columns: IColumn[] = [ { key: "column1", - name: "", + name: "Description", + iconName: "Info", + isIconOnly: true, minWidth: 10, maxWidth: 12, + iconClassName: "iconheadercell", data: String, fieldName: "", onRender: (item: IDocument) => { @@ -378,9 +381,13 @@ export const QueryResultSection: React.FC = ({ Error - We have detected you may be using a subquery. Non-correlated subqueries are not currently supported. - - Please see Cosmos sub query documentation for further information + We detected you may be using a subquery. To learn more about subqueries effectively,{" "} + + visit the documentation diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx index 47c2ce2ba..f297f91f4 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTab.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx @@ -1,3 +1,5 @@ +import { sendMessage } from "Common/MessageHandler"; +import { MessageTypes } from "Contracts/MessageTypes"; import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext"; import { userContext } from "UserContext"; import React from "react"; @@ -54,6 +56,11 @@ export class NewQueryTab extends TabsBase { ); } + public onActivate(): void { + this.propagateTabInformation(MessageTypes.ActivateTab); + super.onActivate(); + } + public onTabClick(): void { useTabs.getState().activateTab(this); this.iTabAccessor.onTabClickEvent(); @@ -61,6 +68,7 @@ export class NewQueryTab extends TabsBase { public onCloseTabButtonClick(): void { useTabs.getState().closeTab(this); + this.propagateTabInformation(MessageTypes.CloseTab); if (this.iTabAccessor) { this.iTabAccessor.onCloseClickEvent(true); } @@ -69,4 +77,15 @@ export class NewQueryTab extends TabsBase { public getContainer(): Explorer { return this.props.container; } + + private propagateTabInformation(type: MessageTypes): void { + sendMessage({ + type, + data: { + kind: this.tabKind, + databaseId: this.collection?.databaseId, + collectionId: this.collection?.id?.(), + }, + }); + } } diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.less b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less index 13daf455c..b0ae00360 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.less +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less @@ -91,9 +91,6 @@ div[role="tabpanel"] { height: 100%; - div:nth-child(1) { - height: 100%; - } } .result-metadata { @@ -283,3 +280,6 @@ } } } +.iconheadercell { + font-size: 12px; +} diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 20cb683d1..532ce4662 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ -import { FeedOptions } from "@azure/cosmos"; +import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; +import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; @@ -9,8 +10,9 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; -import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { TabsState, useTabs } from "hooks/useTabs"; @@ -20,6 +22,7 @@ import "react-splitter-layout/lib/index.css"; import { format } from "react-string-format"; import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; +import DownloadQueryIcon from "../../../../images/DownloadQuery.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg"; @@ -133,7 +136,7 @@ export default class QueryTabComponent extends React.Component { + const text = this.getCurrentEditorQuery(); + const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" }); + + // It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it + const blobUrl = URL.createObjectURL(queryFile); + const anchor = document.createElement("a"); + anchor.href = blobUrl; + anchor.download = queryFile.name; + document.body.appendChild(anchor); // Must put the anchor in the document. + anchor.click(); + document.body.removeChild(anchor); // Clean up the anchor. + }; + public onSaveQueryClick = (): void => { useSidePanel.getState().openSidePanel("Save Query", ); }; @@ -303,8 +320,20 @@ export default class QueryTabComponent extends React.Component - await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex); + await queryDocumentsPage( + this.props.collection && this.props.collection.id(), + this._iterator, + firstItemIndex, + queryOperationOptions, + ); this.props.tabsBaseInstance.isExecuting(true); this.setState({ isExecuting: true, @@ -380,6 +409,7 @@ export default class QueryTabComponent extends React.Component OnExecuteQueryClick(this.props.copilotStore) : this.onExecuteQueryClick, @@ -391,13 +421,27 @@ export default class QueryTabComponent extends React.Component { this._toggleCopilot(!this.state.copilotActive); }, - commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", - ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", + commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", + ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", hasPopup: false, }; buttons.push(toggleCopilotButton); @@ -455,6 +500,7 @@ export default class QueryTabComponent extends React.Component this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, @@ -483,13 +529,16 @@ export default class QueryTabComponent extends React.Component 0) { this.executeQueryButton = { @@ -504,6 +553,8 @@ export default class QueryTabComponent extends React.Component 0; + useCommandBar.getState().setContextButtons(this.getTabsButtons()); } @@ -531,7 +582,7 @@ export default class QueryTabComponent extends React.Component { this.collection.container.openExecuteSprocParamsPanel(this.node); }, @@ -512,7 +517,12 @@ export default class StoredProcedureTabComponent extends React.Component< return (
-
Stored Procedure Id
+
+ Stored Procedure Id + + *  + +
{ const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs(); + const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState( + userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(), + ); + const [ + showMongoAndCassandraProxiesNetworkSettingsWarningState, + setShowMongoAndCassandraProxiesNetworkSettingsWarningState, + ] = useState(showMongoAndCassandraProxiesNetworkSettingsWarning()); + + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS); + useEffect(() => { + setKeyboardHandlers({ + [KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(), + [KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(), + [KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(), + }); + }, [setKeyboardHandlers]); return (
@@ -54,6 +77,39 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { {networkSettingsWarning} )} + {showRUThresholdMessageBar && ( + { + setShowRUThresholdMessageBar(false); + }} + styles={{ + innerText: { + fontWeight: "bold", + }, + }} + > + {`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove + the limit, go to the Settings cog on the right and find "RU Threshold".`} + + Learn More + + + )} + {showMongoAndCassandraProxiesNetworkSettingsWarningState && ( + { + setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false); + }} + > + {`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please + re-enable "Allow access from Azure Portal" on the Networking blade for your account.`} + + )}
@@ -255,6 +309,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => { }; const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { + // React tabs have no context buttons. + useCommandBar.getState().setContextButtons([]); + // eslint-disable-next-line no-console switch (activeReactTab) { case ReactTabKind.Connect: @@ -279,3 +336,69 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); } }; + +const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { + const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; + if ( + ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) || + (userContext.apiType === "Cassandra" && + configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && + ipRules?.length + ) { + const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; + const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); + const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) => + ipAddressesFromIPRules.includes(legacyPortalBackendIP), + ); + if (!ipRulesIncludeLegacyPortalBackend) { + return false; + } + + if (userContext.apiType === "Mongo") { + const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes( + configContext.MONGO_PROXY_ENDPOINT, + ); + + const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint + ? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]] + : MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT]; + + const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) => + ipAddressesFromIPRules.includes(mongoProxyOutboundIP), + ); + + if (ipRulesIncludeMongoProxy) { + updateConfigContext({ + MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, + }); + } + + return !ipRulesIncludeMongoProxy; + } else if (userContext.apiType === "Cassandra") { + const isProdOrMpacCassandraProxyEndpoint: boolean = [ + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + ].includes(configContext.CASSANDRA_PROXY_ENDPOINT); + + const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint + ? [ + ...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac], + ...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod], + ] + : CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT]; + + const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every( + (cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP), + ); + + if (ipRulesIncludeCassandraProxy) { + updateConfigContext({ + CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, + }); + } + + return !ipRulesIncludeCassandraProxy; + } + } + return false; +}; diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index a6f3a45dd..8b017f6bc 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -1,3 +1,4 @@ +import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as ThemeUtility from "../../Common/ThemeUtility"; @@ -40,11 +41,10 @@ export default class TabsBase extends WaitsForTemplateViewModel { this.database = options.database; this.rid = options.rid || (this.collection && this.collection.rid) || ""; this.tabKind = options.tabKind; - this.tabTitle = ko.observable(options.title); + this.tabTitle = ko.observable(this.getTitle(options)); this.tabPath = - ko.observable(options.tabPath ?? "") || - (this.collection && - ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`)); + this.collection && + ko.observable(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`); this.pendingNotification = ko.observable(undefined); this.onLoadStartKey = options.onLoadStartKey; this.closeTabButton = { @@ -108,6 +108,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onActivate(): void { + clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); this.updateSelectedNode(); this.collection?.selectedSubnodeKind(this.tabKind); this.database?.selectedSubnodeKind(this.tabKind); @@ -143,6 +144,26 @@ export default class TabsBase extends WaitsForTemplateViewModel { return (this.collection && this.collection.container) || (this.database && this.database.container); } + public getTitle(options: ViewModels.TabOptions): string { + const coll = this.collection?.id(); + const db = this.database?.id(); + if (coll) { + if (coll.length > 8) { + return coll.slice(0, 5) + "…" + options.title; + } else { + return coll + "." + options.title; + } + } else if (db) { + if (db.length > 8) { + return db.slice(0, 5) + "…" + options.title; + } else { + return db + "." + options.title; + } + } else { + return options.title; + } + } + /** Renders a Javascript object to be displayed inside Monaco Editor */ public renderObjectForEditor(value: any, replacer: any, space: string | number): string { return JSON.stringify(value, replacer, space); diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 538c596c5..986d4ef11 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -34,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { private getTabId: () => string, private getUsername: () => string, private isAllPublicIPAddressesEnabled: ko.Observable, + private kind: ViewModels.TerminalKind, ) {} public renderComponent(): JSX.Element { @@ -42,7 +43,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { ); } @@ -58,6 +59,18 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { ); } + + private getShellNameForDisplay(terminalKind: ViewModels.TerminalKind): string { + switch (terminalKind) { + case ViewModels.TerminalKind.Postgres: + return "PostgreSQL"; + case ViewModels.TerminalKind.Mongo: + case ViewModels.TerminalKind.VCoreMongo: + return "MongoDB"; + default: + return ""; + } + } } export default class TerminalTab extends TabsBase { @@ -76,6 +89,7 @@ export default class TerminalTab extends TabsBase { () => this.tabId, () => this.getUsername(), this.isAllPublicIPAddressesEnabled, + options.kind, ); this.notebookTerminalComponentAdapter.parameters = ko.computed(() => { if ( diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 315984554..5fd28502a 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -1,12 +1,13 @@ import { TriggerDefinition } from "@azure/cosmos"; import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; +import { KeyboardAction } from "KeyboardShortcuts"; import React, { Component } from "react"; import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; import * as Constants from "../../Common/Constants"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { createTrigger } from "../../Common/dataAccess/createTrigger"; import { updateTrigger } from "../../Common/dataAccess/updateTrigger"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -218,6 +219,18 @@ export class TriggerTabContent extends Component diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx index c0d78ff9a..98b90c045 100644 --- a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx +++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx @@ -1,12 +1,13 @@ import { UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Label, TextField } from "@fluentui/react"; +import { KeyboardAction } from "KeyboardShortcuts"; import React, { Component } from "react"; import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; import * as Constants from "../../Common/Constants"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction"; import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -80,6 +81,7 @@ export default class UserDefinedFunctionTabContent extends Component< setState: this.setState, iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveClick, commandButtonLabel: label, ariaLabel: label, @@ -94,6 +96,7 @@ export default class UserDefinedFunctionTabContent extends Component< ...this, iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onUpdateClick, commandButtonLabel: label, ariaLabel: label, @@ -109,6 +112,7 @@ export default class UserDefinedFunctionTabContent extends Component< ...this, iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onDiscard, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 4b5411d09..d7c673620 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -58,6 +58,7 @@ export default class Collection implements ViewModels.Collection { public indexingPolicy: ko.Observable; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public usageSizeInKB: ko.Observable; + public computedProperties: ko.Observable; public offer: ko.Observable; public conflictResolutionPolicy: ko.Observable; @@ -121,6 +122,7 @@ export default class Collection implements ViewModels.Collection { this.schema = data.schema; this.requestSchema = data.requestSchema; this.geospatialConfig = ko.observable(data.geospatialConfig); + this.computedProperties = ko.observable(data.computedProperties); this.partitionKeyPropertyHeaders = this.partitionKey?.paths; this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => { @@ -306,7 +308,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.rawDataModel.id + " - Items", + tabTitle: "Items", }); this.documentIds([]); @@ -314,7 +316,7 @@ export default class Collection implements ViewModels.Collection { partitionKey: this.partitionKey, documentIds: ko.observableArray([]), tabKind: ViewModels.CollectionTabKind.Documents, - title: this.rawDataModel.id + " - Items", + title: "Items", collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Documents`, diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b245f327f..a14c202e8 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,11 +1,9 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; @@ -14,17 +12,14 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; @@ -36,7 +31,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -75,152 +69,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); const pseudoDirPath = "PsuedoDir"; - const buildGalleryCallout = (): JSX.Element => { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - container.openGallery(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - }; - - const buildNotebooksTree = (): TreeNode => { - const notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (!useNotebook.getState().isPhoenixNotebooks) { - notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); - } else { - if (galleryContentRoot) { - notebooksTree.children.push(buildGalleryNotebooksTree()); - } - - if ( - myNotebooksContentRoot && - useNotebook.getState().isPhoenixNotebooks && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected - ) { - notebooksTree.children.push(buildMyNotebooksTree()); - } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } - } - return notebooksTree; - }; - - const buildNotebooksTemporarilyDownTree = (): TreeNode => { - return { - label: Notebook.temporarilyDownMsg, - className: "clickDisabled", - }; - }; - - const buildGalleryNotebooksTree = (): TreeNode => { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => container.openGallery(), - isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, - }; - }; - - const buildMyNotebooksTree = (): TreeNode => { - const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( - myNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - }; - - const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { - const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( - gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - true, - ); - const manageGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - gitHubNotebooksTree.contextMenu = manageGitContextMenu; - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - }; - const buildChildNodes = ( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, @@ -373,16 +221,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc iconSrc: NewNotebookIcon, onClick: () => container.onCreateDirectory(item, isGithubTree), }, - { - label: "New Notebook", - iconSrc: NewNotebookIcon, - onClick: () => container.onNewNotebookClicked(item, isGithubTree), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => container.openUploadFilePanel(item), - }, ]; //disallow renaming of temporary notebook workspace @@ -786,12 +624,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - - - - {/* {buildGalleryCallout()} */} )} {!isNotebookEnabled && isSampleDataEnabled && ( @@ -804,8 +637,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - {/* {buildGalleryCallout()} */} )} {isNotebookEnabled && isSampleDataEnabled && ( @@ -817,12 +648,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - - - - {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index abe05e85f..2fd8d6bd1 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,9 +1,7 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; @@ -13,21 +11,17 @@ import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { IPinnedRepo } from "../../Juno/JunoClient"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; @@ -36,7 +30,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -102,26 +95,7 @@ export class ResourceTreeAdapter implements ReactAdapter { public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - const notebooksRootNode = this.buildNotebooksTrees(); - - if (useNotebook.getState().isNotebookEnabled) { - return ( - <> - - - - - - - - - - {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} - - ); - } else { - return ; - } + return ; } public async initialize(): Promise { @@ -504,156 +478,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } - private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (this.galleryContentRoot) { - notebooksTree.children.push(this.buildGalleryNotebooksTree()); - } - - if (this.myNotebooksContentRoot) { - notebooksTree.children.push(this.buildMyNotebooksTree()); - } - - if (this.gitHubNotebooksContentRoot) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(this.buildGitHubNotebooksTree()); - } - - return notebooksTree; - } - - private buildGalleryCallout(): JSX.Element { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.triggerRender(); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.container.openGallery(); - this.triggerRender(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - } - - private buildGalleryNotebooksTree(): TreeNode { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => this.container.openGallery(), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - }, - }; - } - - private buildMyNotebooksTree(): TreeNode { - const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - } - - private buildGitHubNotebooksTree(): TreeNode { - const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - gitHubNotebooksTree.contextMenu = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - this.container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - } - private buildChildNodes( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, @@ -800,16 +624,6 @@ export class ResourceTreeAdapter implements ReactAdapter { iconSrc: NewNotebookIcon, onClick: () => this.container.onCreateDirectory(item), }, - { - label: "New Notebook", - iconSrc: NewNotebookIcon, - onClick: () => this.container.onNewNotebookClicked(item), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => this.container.openUploadFilePanel(item), - }, ]; //disallow renaming of temporary notebook workspace diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index 7701f1360..4d1155692 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -68,7 +68,9 @@ export const useDatabases: UseStore = create((set, get) => ({ return true; }, findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => { - return get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase); + return isSampleDatabase === undefined + ? get().databases.find((db) => databaseId === db.id()) + : get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase); }, isLastNonEmptyDatabase: () => { const databases = get().databases; diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 1eb2f5711..6f9c62866 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -1,5 +1,6 @@ import { initializeIcons } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; +import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure"; import * as React from "react"; import { render } from "react-dom"; import ChevronRight from "../images/chevron-right.svg"; @@ -32,7 +33,8 @@ const App: React.FunctionComponent = () => { // For showing/hiding panel const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); const config = useConfig(); - const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); + const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } = + useAADAuth(); const [databaseAccount, setDatabaseAccount] = React.useState(); const [authType, setAuthType] = React.useState(encryptedToken ? AuthType.EncryptedToken : undefined); const [connectionString, setConnectionString] = React.useState(); @@ -136,7 +138,10 @@ const App: React.FunctionComponent = () => { {!isLoggedIn && !encryptedTokenMetadata && ( )} - {isLoggedIn && } + {isLoggedIn && authFailure && } + {isLoggedIn && !authFailure && ( + + )} ); }; diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 7cef27da1..0b0618e72 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -1,6 +1,6 @@ -import ko from "knockout"; -import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation"; +import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils"; import { GetGithubClientId } from "Utils/GitHubUtils"; +import ko from "knockout"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx new file mode 100644 index 000000000..2041662ee --- /dev/null +++ b/src/KeyboardShortcuts.tsx @@ -0,0 +1,183 @@ +import * as React from "react"; +import { PropsWithChildren, useEffect } from "react"; +import { KeyBindingMap, tinykeys } from "tinykeys"; +import create, { UseStore } from "zustand"; + +/** + * Represents a keyboard shortcut handler. + * Return `true` to prevent the default action of the keyboard shortcut. + * Any other return value will allow the default action to proceed. + */ +export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void; + +export type KeyboardHandlerMap = Partial>; + +/** + * The groups of keyboard actions that can be managed by the application. + * Each group can be updated separately, but, when updated, must be completely replaced. + */ +export enum KeyboardActionGroup { + /** Keyboard actions related to tab navigation. */ + TABS = "TABS", + + /** Keyboard actions managed by the global command bar. */ + COMMAND_BAR = "COMMAND_BAR", + + /** + * Keyboard actions specific to the active tab. + * This group is automatically cleared when the active tab changes. + */ + ACTIVE_TAB = "ACTIVE_TAB", +} + +/** + * The possible actions that can be triggered by keyboard shortcuts. + */ +export enum KeyboardAction { + NEW_QUERY = "NEW_QUERY", + EXECUTE_ITEM = "EXECUTE_ITEM", + CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD", + SAVE_ITEM = "SAVE_ITEM", + DOWNLOAD_ITEM = "DOWNLOAD_ITEM", + OPEN_QUERY = "OPEN_QUERY", + OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK", + NEW_SPROC = "NEW_SPROC", + NEW_UDF = "NEW_UDF", + NEW_TRIGGER = "NEW_TRIGGER", + NEW_DATABASE = "NEW_DATABASE", + NEW_COLLECTION = "NEW_CONTAINER", + NEW_ITEM = "NEW_ITEM", + DELETE_ITEM = "DELETE_ITEM", + TOGGLE_COPILOT = "TOGGLE_COPILOT", + SELECT_LEFT_TAB = "SELECT_LEFT_TAB", + SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB", + CLOSE_TAB = "CLOSE_TAB", + SEARCH = "SEARCH", + CLEAR_SEARCH = "CLEAR_SEARCH", +} + +/** + * The keyboard shortcuts for the application. + * This record maps each action to the keyboard shortcuts that trigger the action. + * Even if an action is specified here, it will not be triggered unless a handler is set for it. + */ +const bindings: Record = { + // NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS. + // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. + + [KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"], + [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"], + [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], + [KeyboardAction.SAVE_ITEM]: ["$mod+S"], + [KeyboardAction.DOWNLOAD_ITEM]: ["$mod+Shift+S"], + [KeyboardAction.OPEN_QUERY]: ["$mod+O"], + [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], + [KeyboardAction.NEW_SPROC]: ["Alt+N P"], + [KeyboardAction.NEW_UDF]: ["Alt+N F"], + [KeyboardAction.NEW_TRIGGER]: ["Alt+N T"], + [KeyboardAction.NEW_DATABASE]: ["Alt+N D"], + [KeyboardAction.NEW_COLLECTION]: ["Alt+N C"], + [KeyboardAction.NEW_ITEM]: ["Alt+N I"], + [KeyboardAction.DELETE_ITEM]: ["Alt+D"], + [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], + [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"], + [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], + [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], + [KeyboardAction.SEARCH]: ["$mod+Shift+F"], + [KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"], +}; + +interface KeyboardShortcutState { + /** + * A set of all the keyboard shortcuts handlers. + */ + allHandlers: KeyboardHandlerMap; + + /** + * A set of all the groups of keyboard shortcuts handlers. + */ + groups: Partial>; + + /** + * Sets the keyboard shortcut handlers for the given group. + */ + setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; +} + +export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void; + +/** + * Defines the calling component as the manager of the keyboard actions for the given group. + * @param group The group of keyboard actions to manage. + * @returns A function that can be used to set the keyboard action handlers for the given group. + */ +export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter = + (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => + useKeyboardActionHandlers.getState().setHandlers(group, handlers); + +/** + * Clears the keyboard action handlers for the given group. + * @param group The group of keyboard actions to clear. + */ +export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => { + useKeyboardActionHandlers.getState().setHandlers(group, {}); +}; + +const useKeyboardActionHandlers: UseStore = create((set, get) => ({ + allHandlers: {}, + groups: {}, + setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => { + const state = get(); + const groups = { ...state.groups, [group]: handlers }; + + // Combine all the handlers from all the groups in the correct order. + const allHandlers: KeyboardHandlerMap = {}; + eachKey(groups).forEach((group) => { + const groupHandlers = groups[group]; + if (groupHandlers) { + eachKey(groupHandlers).forEach((action) => { + // Check for duplicate handlers in development mode. + // We don't want to raise an error here in production, but having duplicate handlers is a mistake. + if (process.env.NODE_ENV === "development" && allHandlers[action]) { + throw new Error(`Duplicate handler for Keyboard Action "${action}".`); + } + allHandlers[action] = groupHandlers[action]; + }); + } + }); + set({ groups, allHandlers }); + }, +})); + +function createHandler(action: KeyboardAction): KeyboardActionHandler { + return (e) => { + const state = useKeyboardActionHandlers.getState(); + const handler = state.allHandlers[action]; + if (handler && handler(e)) { + e.preventDefault(); + e.stopPropagation(); + } + }; +} + +const allHandlers: KeyBindingMap = {}; +eachKey(bindings).forEach((action) => { + const shortcuts = bindings[action]; + shortcuts.forEach((shortcut) => { + allHandlers[shortcut] = createHandler(action); + }); +}); + +export function KeyboardShortcutRoot({ children }: PropsWithChildren) { + useEffect(() => { + // We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component. + tinykeys(document.body, allHandlers); + }, []); + + return <>{children}; +} + +/** A _typed_ version of `Object.keys` that preserves the original key type */ +function eachKey(record: Partial>): K[] { + return Object.keys(record) as K[]; +} diff --git a/src/Main.tsx b/src/Main.tsx index d62f9b45e..f79ee29f1 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -1,3 +1,6 @@ +// Import this first, to ensure that the dev tools hook is copied before React is loaded. +import "./ReactDevTools"; + // CSS Dependencies import { initializeIcons, loadTheme } from "@fluentui/react"; import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; @@ -18,6 +21,7 @@ import "../externals/jquery.typeahead.min.js"; // Image Dependencies import { Platform } from "ConfigContext"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; +import { KeyboardShortcutRoot } from "KeyboardShortcuts"; import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import "../images/favicon.ico"; @@ -88,52 +92,54 @@ const App: React.FunctionComponent = () => { } return ( -
-
-
- {/* Main Command Bar - Start */} - - {/* Collections Tree and Tabs - Begin */} -
- {/* Collections Tree - Start */} - {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( -
-
- {/* Collections Tree Expanded - Start */} - - {/* Collections Tree Expanded - End */} - {/* Collections Tree Collapsed - Start */} - - {/* Collections Tree Collapsed - End */} + +
+
+
+ {/* Main Command Bar - Start */} + + {/* Collections Tree and Tabs - Begin */} +
+ {/* Collections Tree - Start */} + {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( +
+
+ {/* Collections Tree Expanded - Start */} + + {/* Collections Tree Expanded - End */} + {/* Collections Tree Collapsed - Start */} + + {/* Collections Tree Collapsed - End */} +
-
- )} - -
- {/* Collections Tree and Tabs - End */} - + {/* Collections Tree and Tabs - End */} +
+ + + {} + {} + {} + {}
- - - {} - {} - {} - {} -
+ ); }; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index 685bbbbb2..d5e304b8e 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -2,7 +2,7 @@ import { configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { userContext } from "UserContext"; -import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation"; +import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import promiseRetry, { AbortError } from "p-retry"; import { diff --git a/src/Platform/Fabric/FabricUtil.ts b/src/Platform/Fabric/FabricUtil.ts index 53c38f988..26ba859ff 100644 --- a/src/Platform/Fabric/FabricUtil.ts +++ b/src/Platform/Fabric/FabricUtil.ts @@ -29,7 +29,11 @@ const requestDatabaseResourceTokens = async (): Promise => { } updateUserContext({ - fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo }, + fabricContext: { + ...userContext.fabricContext, + databaseConnectionInfo: fabricDatabaseConnectionInfo, + isReadOnly: true, + }, databaseAccount: { ...userContext.databaseAccount }, }); scheduleRefreshDatabaseResourceToken(); diff --git a/src/Platform/Hosted/AadAuthorizationFailure.less b/src/Platform/Hosted/AadAuthorizationFailure.less new file mode 100644 index 000000000..696a8337b --- /dev/null +++ b/src/Platform/Hosted/AadAuthorizationFailure.less @@ -0,0 +1,52 @@ +.aadAuthFailureContainer { + height: 100%; + width: 100%; +} +.aadAuthFailureContainer .aadAuthFailureFormContainer { + display: -webkit-flex; + display: -ms-flexbox; + display: -ms-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + width: 100%; +} +.aadAuthFailureContainer .aadAuthFailure { + text-align: center; + display: -webkit-flex; + display: -ms-flexbox; + display: -ms-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + justify-content: center; + height: 100%; + margin-bottom: 60px; +} +.aadAuthFailureContainer .aadAuthFailure .authFailureTitle { + font-size: 16px; + font-weight: 500; + color: #d12d2d; + margin: 16px 8px 8px 8px; +} +.aadAuthFailureContainer .aadAuthFailure .authFailureMessage { + font-size: 14px; + color: #393939; + margin: 16px 16px 16px 16px; + word-wrap: break-word; + white-space: pre-wrap; +} +.aadAuthFailureContainer .aadAuthFailure .authFailureLink { + margin: 8px; + font-size: 14px; + color: #0058ad; + cursor: pointer; +} + +.aadAuthFailureContainer .aadAuthFailure .aadAuthFailureContent { + margin: 8px; + color: #393939; +} diff --git a/src/Platform/Hosted/Components/AadAuthorizationFailure.tsx b/src/Platform/Hosted/Components/AadAuthorizationFailure.tsx new file mode 100644 index 000000000..00d360a2e --- /dev/null +++ b/src/Platform/Hosted/Components/AadAuthorizationFailure.tsx @@ -0,0 +1,29 @@ +import { AadAuthFailure } from "hooks/useAADAuth"; +import * as React from "react"; +import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; +import "../AadAuthorizationFailure.less"; + +interface Props { + authFailure: AadAuthFailure; +} + +export const AadAuthorizationFailure: React.FunctionComponent = ({ authFailure }: Props) => { + return ( +
+
+
+

+ Azure Cosmos DB +

+

Authorization Failure

+

{authFailure.failureMessage}

+ {authFailure.failureLinkTitle && ( +

+ {authFailure.failureLinkTitle} +

+ )} +
+
+
+ ); +}; diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index dd291f279..749f248ba 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -1,10 +1,11 @@ import { useBoolean } from "@fluentui/react-hooks"; import { userContext } from "UserContext"; +import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import * as React from "react"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ErrorImage from "../../../../images/error.svg"; import { AuthType } from "../../../AuthType"; -import { HttpHeaders } from "../../../Common/Constants"; +import { BackendApi, HttpHeaders } from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; import { GenerateTokenResponse } from "../../../Contracts/DataModels"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; @@ -18,6 +19,23 @@ interface Props { } export const fetchEncryptedToken = async (connectionString: string): Promise => { + if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) { + return await fetchEncryptedToken_ToBeDeprecated(connectionString); + } + + const headers = new Headers(); + headers.append(HttpHeaders.connectionString, connectionString); + const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken"; + const response = await fetch(url, { headers, method: "POST" }); + if (!response.ok) { + throw response; + } + + const encryptedTokenResponse: string = await response.json(); + return decodeURIComponent(encryptedTokenResponse); +}; + +export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise => { const headers = new Headers(); headers.append(HttpHeaders.connectionString, connectionString); const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken"; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 7bf3c8a3f..5bd84516e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -31,11 +31,6 @@ export type Features = { readonly mongoProxyAPIs?: string; readonly enableThroughputCap: boolean; readonly enableHierarchicalKeys: boolean; - readonly enableLegacyMongoShellV1: boolean; - readonly enableLegacyMongoShellV1Debug: boolean; - readonly enableLegacyMongoShellV2: boolean; - readonly enableLegacyMongoShellV2Debug: boolean; - readonly loadLegacyMongoShellFromBE: boolean; readonly enableCopilot: boolean; readonly copilotVersion?: string; readonly disableCopilotPhoenixGateaway: boolean; @@ -106,11 +101,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear notebooksDownBanner: "true" === get("notebooksDownBanner"), enableThroughputCap: "true" === get("enablethroughputcap"), enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"), - enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"), - enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"), - enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"), - enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"), - loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"), enableCopilot: "true" === get("enablecopilot", "true"), copilotVersion: get("copilotversion") ?? "v2.0", disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"), diff --git a/src/ReactDevTools.ts b/src/ReactDevTools.ts index 09947f934..2a12d81d5 100644 --- a/src/ReactDevTools.ts +++ b/src/ReactDevTools.ts @@ -1,3 +1,7 @@ if (window.parent !== window) { - (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; + try { + (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = (window.parent as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; + } catch { + // No-op. We can throw here if the parent is not the same origin (such as in the Azure portal). + } } diff --git a/src/Shared/Constants.ts b/src/Shared/Constants.ts index 1c2e91cfb..d26a5700c 100644 --- a/src/Shared/Constants.ts +++ b/src/Shared/Constants.ts @@ -172,7 +172,6 @@ export class CollectionCreation { public static readonly DefaultCollectionRUs100K: number = 100000; public static readonly DefaultCollectionRUs1Million: number = 1000000; - public static readonly DefaultAddCollectionDefaultFlight: string = "0"; public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free; public static readonly TablesAPIDefaultDatabase: string = "TablesDB"; diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index ca7bc81af..b229ac7db 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -1,9 +1,12 @@ import * as LocalStorageUtility from "./LocalStorageUtility"; import * as SessionStorageUtility from "./SessionStorageUtility"; +import * as StringUtility from "./StringUtility"; export { LocalStorageUtility, SessionStorageUtility }; export enum StorageKey { ActualItemPerPage, + RUThresholdEnabled, + RUThreshold, QueryTimeoutEnabled, QueryTimeout, RetryAttempts, @@ -25,3 +28,27 @@ export enum StorageKey { VisitedAccounts, PriorityLevel, } + +export const hasRUThresholdBeenConfigured = (): boolean => { + const ruThresholdEnabledLocalStorageRaw: string | null = LocalStorageUtility.getEntryString( + StorageKey.RUThresholdEnabled, + ); + return ruThresholdEnabledLocalStorageRaw === "true" || ruThresholdEnabledLocalStorageRaw === "false"; +}; + +export const ruThresholdEnabled = (): boolean => { + const ruThresholdEnabledLocalStorageRaw: string | null = LocalStorageUtility.getEntryString( + StorageKey.RUThresholdEnabled, + ); + return ruThresholdEnabledLocalStorageRaw === null || StringUtility.toBoolean(ruThresholdEnabledLocalStorageRaw); +}; + +export const getRUThreshold = (): number => { + const ruThresholdRaw = LocalStorageUtility.getEntryNumber(StorageKey.RUThreshold); + if (ruThresholdRaw !== 0) { + return ruThresholdRaw; + } + return DefaultRUThreshold; +}; + +export const DefaultRUThreshold = 5000; diff --git a/src/UserContext.ts b/src/UserContext.ts index d4b64f422..2fa1bb946 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -50,8 +50,25 @@ export interface VCoreMongoConnectionParams { interface FabricContext { connectionId: string; databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; + isReadOnly: boolean; + isVisible: boolean; } +export type AdminFeedbackControlPolicy = + | "connectedExperiences" + | "policyAllowFeedback" + | "policyAllowSurvey" + | "policyAllowScreenshot" + | "policyAllowContact" + | "policyAllowContent" + | "policyEmailCollectionDefault" + | "policyScreenshotDefault" + | "policyContentSamplesDefault"; + +export type AdminFeedbackPolicySettings = { + [key in AdminFeedbackControlPolicy]: boolean; +}; + interface UserContext { readonly fabricContext?: FabricContext; readonly authType?: AuthType; @@ -72,7 +89,6 @@ interface UserContext { readonly isTryCosmosDBSubscription?: boolean; readonly portalEnv?: PortalEnv; readonly features: Features; - readonly addCollectionFlight: string; readonly hasWriteAccess: boolean; readonly parsedResourceToken?: { databaseId: string; @@ -84,10 +100,11 @@ interface UserContext { collectionCreationDefaults: CollectionCreationDefaults; sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; + readonly feedbackPolicies?: AdminFeedbackPolicySettings; } export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; -export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev"; +export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod1" | "rx" | "ex" | "prod" | "dev"; const ONE_WEEK_IN_MS = 604800000; @@ -99,7 +116,6 @@ const userContext: UserContext = { isTryCosmosDBSubscription: false, portalEnv: "prod", features, - addCollectionFlight: CollectionCreation.DefaultAddCollectionDefaultFlight, subscriptionType: CollectionCreation.DefaultSubscriptionType, collectionCreationDefaults: CollectionCreationDefaults, }; diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 7fe1709c0..35fd4ed39 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -1,9 +1,11 @@ import * as msal from "@azure/msal-browser"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; import { AuthType } from "../AuthType"; import * as Constants from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { configContext } from "../ConfigContext"; import * as ViewModels from "../Contracts/ViewModels"; +import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../UserContext"; export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { @@ -43,8 +45,8 @@ export function decryptJWTToken(token: string) { return JSON.parse(tokenPayload); } -export function getMsalInstance() { - const config: msal.Configuration = { +export async function getMsalInstance() { + const msalConfig: msal.Configuration = { cache: { cacheLocation: "localStorage", }, @@ -55,8 +57,46 @@ export function getMsalInstance() { }; if (process.env.NODE_ENV === "development") { - config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; + msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net"; } - const msalInstance = new msal.PublicClientApplication(config); + + const msalInstance = new msal.PublicClientApplication(msalConfig); return msalInstance; } + +export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) { + const tokenRequest = { + account: msalInstance.getActiveAccount() || null, + ...request, + }; + + try { + // attempt silent acquisition first + return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken; + } catch (silentError) { + if (silentError instanceof msal.InteractionRequiredAuthError) { + try { + // The error indicates that we need to acquire the token interactively. + // This will display a pop-up to re-establish authorization. If user does not + // have pop-ups enabled in their browser, this will fail. + return (await msalInstance.acquireTokenPopup(tokenRequest)).accessToken; + } catch (interactiveError) { + traceFailure(Action.SignInAad, { + request: JSON.stringify(tokenRequest), + acquireTokenType: "interactive", + errorMessage: JSON.stringify(interactiveError), + }); + + throw interactiveError; + } + } else { + traceFailure(Action.SignInAad, { + request: JSON.stringify(tokenRequest), + acquireTokenType: "silent", + errorMessage: JSON.stringify(silentError), + }); + + throw silentError; + } + } +} diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index fc2de8149..8b6976666 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -9,4 +9,14 @@ export const isCapabilityEnabled = (capabilityName: string): boolean => { return false; }; -export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless); +export const isServerlessAccount = (): boolean => { + const { databaseAccount } = userContext; + return ( + databaseAccount?.properties?.capacityMode === Constants.CapacityMode.Serverless || + isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) + ); +}; + +export const isVectorSearchEnabled = (): boolean => { + return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch); +}; diff --git a/src/Utils/CloudUtils.ts b/src/Utils/CloudUtils.ts index 089bbf0b6..2593aa4dc 100644 --- a/src/Utils/CloudUtils.ts +++ b/src/Utils/CloudUtils.ts @@ -1,9 +1,9 @@ import { userContext } from "../UserContext"; export function isRunningOnNationalCloud(): boolean { - return ( - userContext.portalEnv === "blackforest" || - userContext.portalEnv === "fairfax" || - userContext.portalEnv === "mooncake" - ); + return !isRunningOnPublicCloud(); +} + +export function isRunningOnPublicCloud(): boolean { + return userContext?.portalEnv === "prod1" || userContext?.portalEnv === "prod"; } diff --git a/src/Utils/EndpointValidation.ts b/src/Utils/EndpointUtils.ts similarity index 56% rename from src/Utils/EndpointValidation.ts rename to src/Utils/EndpointUtils.ts index 0f9eb44ef..97e733b98 100644 --- a/src/Utils/EndpointValidation.ts +++ b/src/Utils/EndpointUtils.ts @@ -1,4 +1,11 @@ -import { JunoEndpoints } from "Common/Constants"; +import { + BackendApi, + CassandraProxyEndpoints, + JunoEndpoints, + MongoProxyEndpoints, + PortalBackendEndpoints, +} from "Common/Constants"; +import { configContext } from "ConfigContext"; import * as Logger from "../Common/Logger"; export function validateEndpoint( @@ -67,7 +74,22 @@ export const PortalBackendIPs: { [key: string]: string[] } = { //usnat: ["7.28.202.68"], }; +export const MongoProxyOutboundIPs: { [key: string]: string[] } = { + [MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"], + [MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"], + [MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"], + [MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"], +}; + export const allowedMongoProxyEndpoints: ReadonlyArray = [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, +]; + +export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray = [ "https://main.documentdb.ext.azure.com", "https://main.documentdb.ext.azure.cn", "https://main.documentdb.ext.azure.us", @@ -75,6 +97,29 @@ export const allowedMongoProxyEndpoints: ReadonlyArray = [ "https://localhost:12901", ]; +export const allowedCassandraProxyEndpoints: ReadonlyArray = [ + CassandraProxyEndpoints.Development, + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + CassandraProxyEndpoints.Fairfax, + CassandraProxyEndpoints.Mooncake, +]; + +export const allowedCassandraProxyEndpoints_ToBeDeprecated: ReadonlyArray = [ + "https://main.documentdb.ext.azure.com", + "https://main.documentdb.ext.azure.cn", + "https://main.documentdb.ext.azure.us", + "https://main.cosmos.ext.azure", + "https://localhost:12901", +]; + +export const CassandraProxyOutboundIPs: { [key: string]: string[] } = { + [CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"], + [CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"], + [CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"], + [CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"], +}; + export const allowedEmulatorEndpoints: ReadonlyArray = ["https://localhost:8081"]; export const allowedMongoBackendEndpoints: ReadonlyArray = ["https://localhost:1234"]; @@ -99,3 +144,31 @@ export const allowedJunoOrigins: ReadonlyArray = [ ]; export const allowedNotebookServerUrls: ReadonlyArray = []; + +// +// Temporary function to determine if a portal backend API is supported by the +// new backend in this environment. +// +// TODO: Remove this function once new backend migration is completed for all environments. +// +export function useNewPortalBackendEndpoint(backendApi: string): boolean { + // This maps backend APIs to the environments supported by the new backend. + const newBackendApiEnvironmentMap: { [key: string]: string[] } = { + [BackendApi.GenerateToken]: [ + PortalBackendEndpoints.Development, + PortalBackendEndpoints.Mpac, + PortalBackendEndpoints.Prod, + ], + [BackendApi.PortalSettings]: [ + PortalBackendEndpoints.Development, + PortalBackendEndpoints.Mpac, + PortalBackendEndpoints.Prod, + ], + }; + + if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) { + return false; + } + + return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT); +} diff --git a/src/Utils/NetworkUtility.test.ts b/src/Utils/NetworkUtility.test.ts index ed22b502e..4ee4b5cd2 100644 --- a/src/Utils/NetworkUtility.test.ts +++ b/src/Utils/NetworkUtility.test.ts @@ -1,7 +1,7 @@ import { resetConfigContext, updateConfigContext } from "ConfigContext"; import { DatabaseAccount, IpRule } from "Contracts/DataModels"; import { updateUserContext } from "UserContext"; -import { PortalBackendIPs } from "Utils/EndpointValidation"; +import { PortalBackendIPs } from "Utils/EndpointUtils"; import { getNetworkSettingsWarningMessage } from "./NetworkUtility"; describe("NetworkUtility tests", () => { diff --git a/src/Utils/NetworkUtility.ts b/src/Utils/NetworkUtility.ts index 8f4624297..96f3ae124 100644 --- a/src/Utils/NetworkUtility.ts +++ b/src/Utils/NetworkUtility.ts @@ -1,7 +1,7 @@ import { configContext } from "ConfigContext"; import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import { userContext } from "UserContext"; -import { PortalBackendIPs } from "Utils/EndpointValidation"; +import { PortalBackendIPs } from "Utils/EndpointUtils"; export const getNetworkSettingsWarningMessage = async ( setStateFunc: (warningMessage: string) => void, diff --git a/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts b/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts index 2a7762328..461e516bf 100644 --- a/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/cassandraResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */ export async function listCassandraKeyspaces( diff --git a/src/Utils/arm/generatedClients/cosmos/collection.ts b/src/Utils/arm/generatedClients/cosmos/collection.ts index 86b116f01..4a9a9c198 100644 --- a/src/Utils/arm/generatedClients/cosmos/collection.ts +++ b/src/Utils/arm/generatedClients/cosmos/collection.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account and collection. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts b/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts index 8b3f407da..6fed487b6 100644 --- a/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionPartition.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given collection, split by partition. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts b/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts index ec0d5e0ea..b33c904d9 100644 --- a/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionPartitionRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts b/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts index 281d98289..984cb146a 100644 --- a/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/collectionRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account, collection and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/database.ts b/src/Utils/arm/generatedClients/cosmos/database.ts index 1d01fcbd7..7b286c4ec 100644 --- a/src/Utils/arm/generatedClients/cosmos/database.ts +++ b/src/Utils/arm/generatedClients/cosmos/database.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account and database. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts index 38fdbd89e..09f17c35b 100644 --- a/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccountRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts index 32aeb05eb..4b52b631b 100644 --- a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the properties of an existing Azure Cosmos DB database account. */ export async function get( diff --git a/src/Utils/arm/generatedClients/cosmos/graphResources.ts b/src/Utils/arm/generatedClients/cosmos/graphResources.ts index 807380575..d51d44d77 100644 --- a/src/Utils/arm/generatedClients/cosmos/graphResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/graphResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the graphs under an existing Azure Cosmos DB database account. */ export async function listGraphs( diff --git a/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts b/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts index ffad0a38a..6e8656400 100644 --- a/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/gremlinResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */ export async function listGremlinDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/locations.ts b/src/Utils/arm/generatedClients/cosmos/locations.ts index 5c28d60f0..6ec4cdc62 100644 --- a/src/Utils/arm/generatedClients/cosmos/locations.ts +++ b/src/Utils/arm/generatedClients/cosmos/locations.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* List Cosmos DB locations and their properties */ export async function list(subscriptionId: string): Promise { diff --git a/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts b/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts index b99584e1c..22b316904 100644 --- a/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/mongoDBResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */ export async function listMongoDBDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/operations.ts b/src/Utils/arm/generatedClients/cosmos/operations.ts index 5a01456ce..ae87fbb48 100644 --- a/src/Utils/arm/generatedClients/cosmos/operations.ts +++ b/src/Utils/arm/generatedClients/cosmos/operations.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists all of the available Cosmos DB Resource Provider operations. */ export async function list(): Promise { diff --git a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts index a66e6294c..d9b5d6dfa 100644 --- a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts +++ b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeId.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given partition key range id. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts index 3625b0859..6ec2ba50a 100644 --- a/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts +++ b/src/Utils/arm/generatedClients/cosmos/partitionKeyRangeIdRegion.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given partition key range id and region. */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/percentile.ts b/src/Utils/arm/generatedClients/cosmos/percentile.ts index b7a8d2841..cbbc751dc 100644 --- a/src/Utils/arm/generatedClients/cosmos/percentile.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentile.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts b/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts index aa9432e8f..88c05dd11 100644 --- a/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentileSourceTarget.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts b/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts index 43f102890..87359e9f0 100644 --- a/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts +++ b/src/Utils/arm/generatedClients/cosmos/percentileTarget.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */ export async function listMetrics( diff --git a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts index 321d68639..049e265e9 100644 --- a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-05-15-preview"; /* Lists the SQL databases under an existing Azure Cosmos DB database account. */ export async function listSqlDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/tableResources.ts b/src/Utils/arm/generatedClients/cosmos/tableResources.ts index 3bc16cf0e..0da78793e 100644 --- a/src/Utils/arm/generatedClients/cosmos/tableResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/tableResources.ts @@ -3,13 +3,13 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ +import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -import { configContext } from "../../../../ConfigContext"; -const apiVersion = "2023-09-15-preview"; +const apiVersion = "2024-02-15-preview"; /* Lists the Tables under an existing Azure Cosmos DB database account. */ export async function listTables( diff --git a/src/Utils/arm/generatedClients/cosmos/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts index 3252b38fe..5272f215b 100644 --- a/src/Utils/arm/generatedClients/cosmos/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -3,7 +3,7 @@ Run "npm run generateARMClients" to regenerate Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs - Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json */ /* The List operation response, that contains the client encryption keys and their properties. */ @@ -566,12 +566,14 @@ export interface DatabaseAccountGetProperties { minimalTlsVersion?: MinimalTlsVersion; /* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ - customerManagedKeyStatus?: CustomerManagedKeyStatus; - + customerManagedKeyStatus?: string; /* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */ enablePriorityBasedExecution?: boolean; /* Enum to indicate default Priority Level of request for Priority Based Execution. */ defaultPriorityLevel?: DefaultPriorityLevel; + + /* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */ + enablePerRegionPerPartitionAutoscale?: boolean; } /* Properties to create and update Azure Cosmos DB database accounts. */ @@ -663,12 +665,14 @@ export interface DatabaseAccountCreateUpdateProperties { minimalTlsVersion?: MinimalTlsVersion; /* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ - customerManagedKeyStatus?: CustomerManagedKeyStatus; - + customerManagedKeyStatus?: string; /* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */ enablePriorityBasedExecution?: boolean; /* Enum to indicate default Priority Level of request for Priority Based Execution. */ defaultPriorityLevel?: DefaultPriorityLevel; + + /* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */ + enablePerRegionPerPartitionAutoscale?: boolean; } /* Parameters to create and update Cosmos DB database accounts. */ @@ -763,12 +767,14 @@ export interface DatabaseAccountUpdateProperties { minimalTlsVersion?: MinimalTlsVersion; /* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ - customerManagedKeyStatus?: CustomerManagedKeyStatus; - + customerManagedKeyStatus?: string; /* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */ enablePriorityBasedExecution?: boolean; /* Enum to indicate default Priority Level of request for Priority Based Execution. */ defaultPriorityLevel?: DefaultPriorityLevel; + + /* Flag to indicate enabling/disabling of Per-Region Per-partition autoscale Preview feature on the account */ + enablePerRegionPerPartitionAutoscale?: boolean; } /* Parameters for patching Azure Cosmos DB database account properties. */ @@ -1229,6 +1235,9 @@ export interface SqlDatabaseResource { export interface SqlContainerResource { /* Name of the Cosmos DB SQL container */ id: string; + + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; + /* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */ indexingPolicy?: IndexingPolicy; @@ -1256,6 +1265,20 @@ export interface SqlContainerResource { /* The configuration for defining Materialized Views. This must be specified only for creating a Materialized View container. */ materializedViewDefinition?: MaterializedViewDefinition; + + /* List of computed properties */ + computedProperties?: ComputedProperty[]; +} + +export interface VectorEmbeddingPolicy { + vectorEmbeddings: VectorEmbedding[]; +} + +export interface VectorEmbedding { + path?: string; + dataType?: string; + dimensions?: number; + distanceFunction?: string; } /* Cosmos DB indexing policy */ @@ -1276,6 +1299,13 @@ export interface IndexingPolicy { /* List of spatial specifics */ spatialIndexes?: SpatialSpec[]; + + vectorIndexes?: VectorIndex[]; +} + +export interface VectorIndex { + path?: string; + type?: string; } /* undocumented */ @@ -1325,6 +1355,14 @@ export interface SpatialSpec { /* Indicates the spatial type of index. */ export type SpatialType = "Point" | "LineString" | "Polygon" | "MultiPolygon"; +/* The definition of a computed property */ +export interface ComputedProperty { + /* The name of a computed property, for example - "cp_lowerName" */ + name?: string; + /* The query that evaluates the value for computed property, for example - "SELECT VALUE LOWER(c.name) FROM c" */ + query?: string; +} + /* The configuration of the partition key to be used for partitioning data into multiple partitions */ export interface ContainerPartitionKey { /* List of paths using which data within the container can be partitioned */ @@ -1929,6 +1967,8 @@ export interface RestoreParametersBase { restoreSource?: string; /* Time to which the account has to be restored (ISO-8601 format). */ restoreTimestampInUtc?: string; + /* Specifies whether the restored account will have Time-To-Live disabled upon the successful restore. */ + restoreWithTtlDisabled?: boolean; } /* Parameters to indicate the information about the restore. */ @@ -2072,19 +2112,5 @@ export type ContinuousTier = "Continuous7Days" | "Continuous30Days"; /* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */ export type MinimalTlsVersion = "Tls" | "Tls11" | "Tls12"; -/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */ -export type CustomerManagedKeyStatus = - | "Access to your account is currently revoked because the Azure Cosmos DB service is unable to obtain the AAD authentication token for the account's default identity; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-active-directory-token-acquisition-error (4000)." - | "Access to your account is currently revoked because the Azure Cosmos DB account's key vault key URI does not follow the expected format; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#improper-syntax-detected-on-the-key-vault-uri-property (4006)." - | "Access to your account is currently revoked because the current default identity no longer has permission to the associated Key Vault key; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#default-identity-is-unauthorized-to-access-the-azure-key-vault-key (4002)." - | "Access to your account is currently revoked because the Azure Key Vault DNS name specified by the account's keyvaultkeyuri property could not be resolved; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#unable-to-resolve-the-key-vaults-dns (4009)." - | "Access to your account is currently revoked because the correspondent key is not found on the specified Key Vault; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-key-vault-resource-not-found (4003)." - | "Access to your account is currently revoked because the Azure Cosmos DB service is unable to wrap or unwrap the key; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#internal-unwrapping-procedure-error (4005)." - | "Access to your account is currently revoked because the Azure Cosmos DB account has an undefined default identity; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#invalid-azure-cosmos-db-default-identity (4015)." - | "Access to your account is currently revoked because the access rules are blocking outbound requests to the Azure Key Vault service; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide (4016)." - | "Access to your account is currently revoked because the correspondent Azure Key Vault was not found; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide#azure-key-vault-resource-not-found (4017)." - | "Access to your account is currently revoked; for more details about this error and how to restore access to your account please visit https://learn.microsoft.com/en-us/azure/cosmos-db/cmk-troubleshooting-guide" - | "Access to the configured customer managed key confirmed."; - /* Enum to indicate default priorityLevel of requests */ export type DefaultPriorityLevel = "High" | "Low"; diff --git a/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts b/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts new file mode 100644 index 000000000..0c2b7c916 --- /dev/null +++ b/src/Utils/arm/generatedClients/dataTransferService/dataTransferJobs.ts @@ -0,0 +1,78 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json +*/ + +import { configContext } from "../../../../ConfigContext"; +import { armRequest } from "../../request"; +import * as Types from "./types"; +const apiVersion = "2023-11-15-preview"; + +/* Creates a Data Transfer Job. */ +export async function create( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, + body: Types.CreateJobRequest, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); +} + +/* Get a Data Transfer Job. */ +export async function get( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} + +/* Pause a Data Transfer Job. */ +export async function pause( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/pause`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Resumes a Data Transfer Job. */ +export async function resume( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/resume`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Cancels a Data Transfer Job. */ +export async function cancel( + subscriptionId: string, + resourceGroupName: string, + accountName: string, + jobName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/cancel`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion }); +} + +/* Get a list of Data Transfer jobs. */ +export async function listByDatabaseAccount( + subscriptionId: string, + resourceGroupName: string, + accountName: string, +): Promise { + const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`; + return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); +} diff --git a/src/Utils/arm/generatedClients/dataTransferService/types.ts b/src/Utils/arm/generatedClients/dataTransferService/types.ts new file mode 100644 index 000000000..27c3db709 --- /dev/null +++ b/src/Utils/arm/generatedClients/dataTransferService/types.ts @@ -0,0 +1,101 @@ +/* + AUTOGENERATED FILE + Run "npm run generateARMClients" to regenerate + Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs + + Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json +*/ + +/* Base class for all DataTransfer source/sink */ +export interface DataTransferDataSourceSink { + /* undocumented */ + component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBSql" | "AzureBlobStorage"; +} + +/* A base CosmosDB data source/sink */ +export type BaseCosmosDataTransferDataSourceSink = DataTransferDataSourceSink & { + /* undocumented */ + remoteAccountName?: string; +}; + +/* A CosmosDB Cassandra API data source/sink */ +export type CosmosCassandraDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { + /* undocumented */ + keyspaceName: string; + /* undocumented */ + tableName: string; +}; + +/* A CosmosDB Mongo API data source/sink */ +export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { + /* undocumented */ + databaseName: string; + /* undocumented */ + collectionName: string; +}; + +/* A CosmosDB No Sql API data source/sink */ +export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { + /* undocumented */ + databaseName: string; + /* undocumented */ + containerName: string; +}; + +/* An Azure Blob Storage data source/sink */ +export type AzureBlobDataTransferDataSourceSink = DataTransferDataSourceSink & { + /* undocumented */ + containerName: string; + /* undocumented */ + endpointUrl?: string; +}; + +/* The properties of a DataTransfer Job */ +export interface DataTransferJobProperties { + /* Job Name */ + readonly jobName?: string; + /* Source DataStore details */ + source: DataTransferDataSourceSink; + + /* Destination DataStore details */ + destination: DataTransferDataSourceSink; + + /* Job Status */ + readonly status?: string; + /* Processed Count. */ + readonly processedCount?: number; + /* Total Count. */ + readonly totalCount?: number; + /* Last Updated Time (ISO-8601 format). */ + readonly lastUpdatedUtcTime?: string; + /* Worker count */ + workerCount?: number; + /* Error response for Faulted job */ + readonly error?: unknown; + + /* Total Duration of Job */ + readonly duration?: string; + /* Mode of job execution */ + mode?: "Offline" | "Online"; +} + +/* Parameters to create Data Transfer Job */ +export type CreateJobRequest = unknown & { + /* Data Transfer Create Job Properties */ + properties: DataTransferJobProperties; +}; + +/* A Cosmos DB Data Transfer Job */ +export type DataTransferJobGetResults = unknown & { + /* undocumented */ + properties?: DataTransferJobProperties; +}; + +/* The List operation response, that contains the Data Transfer jobs and their properties. */ +export interface DataTransferJobFeedResults { + /* List of Data Transfer jobs and their properties. */ + readonly value?: DataTransferJobGetResults[]; + + /* URL to get the next set of Data Transfer job list results if there are any. */ + readonly nextLink?: string; +} diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 749a39632..c20f953f7 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -2,9 +2,9 @@ import * as msal from "@azure/msal-browser"; import { useBoolean } from "@fluentui/react-hooks"; import * as React from "react"; import { configContext } from "../ConfigContext"; -import { getMsalInstance } from "../Utils/AuthorizationUtils"; +import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils"; -const msalInstance = getMsalInstance(); +const msalInstance = await getMsalInstance(); const cachedAccount = msalInstance.getAllAccounts()?.[0]; const cachedTenantId = localStorage.getItem("cachedTenantId"); @@ -18,6 +18,13 @@ interface ReturnType { tenantId: string; account: msal.AccountInfo; switchTenant: (tenantId: string) => void; + authFailure: AadAuthFailure; +} + +export interface AadAuthFailure { + failureMessage: string; + failureLinkTitle?: string; + failureLinkAction?: () => void; } export function useAADAuth(): ReturnType { @@ -28,6 +35,7 @@ export function useAADAuth(): ReturnType { const [tenantId, setTenantId] = React.useState(cachedTenantId); const [graphToken, setGraphToken] = React.useState(); const [armToken, setArmToken] = React.useState(); + const [authFailure, setAuthFailure] = React.useState(undefined); msalInstance.setActiveAccount(account); const login = React.useCallback(async () => { @@ -61,24 +69,60 @@ export function useAADAuth(): ReturnType { [account, tenantId], ); - React.useEffect(() => { - if (account && tenantId) { - Promise.all([ - msalInstance.acquireTokenSilent({ - authority: `${configContext.AAD_ENDPOINT}${tenantId}`, - scopes: [`${configContext.GRAPH_ENDPOINT}/.default`], - }), - msalInstance.acquireTokenSilent({ - authority: `${configContext.AAD_ENDPOINT}${tenantId}`, - scopes: [`${configContext.ARM_ENDPOINT}/.default`], - }), - ]).then(([graphTokenResponse, armTokenResponse]) => { - setGraphToken(graphTokenResponse.accessToken); - setArmToken(armTokenResponse.accessToken); + const acquireTokens = React.useCallback(async () => { + if (!(account && tenantId)) { + return; + } + + try { + const armToken = await acquireTokenWithMsal(msalInstance, { + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.ARM_ENDPOINT}/.default`], }); + + setArmToken(armToken); + setAuthFailure(null); + } catch (error) { + if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) { + // This error can occur when acquireTokenWithMsal() has attempted to acquire token interactively + // and user has popups disabled in browser. This fails as the popup is not the result of a explicit user + // action. In this case, we display the failure and a link to repeat the operation. Clicking on the + // link is a user action so it will work even if popups have been disabled. + // See: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/76#issuecomment-324787539 + setAuthFailure({ + failureMessage: + "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease click below to retry authorization without requiring popups being enabled.", + failureLinkTitle: "Retry Authorization", + failureLinkAction: acquireTokens, + }); + } else { + const errorJson = JSON.stringify(error); + setAuthFailure({ + failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`, + }); + } + } + + try { + const graphToken = await acquireTokenWithMsal(msalInstance, { + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.GRAPH_ENDPOINT}/.default`], + }); + + setGraphToken(graphToken); + } catch (error) { + // Graph token is used only for retrieving user photo at the moment, so + // it's not critical if this fails. + console.warn("Error acquiring graph token: " + error); } }, [account, tenantId]); + React.useEffect(() => { + if (account && tenantId && !authFailure) { + acquireTokens(); + } + }, [account, tenantId, acquireTokens, authFailure]); + return { account, tenantId, @@ -88,5 +132,6 @@ export function useAADAuth(): ReturnType { login, logout, switchTenant, + authFailure, }; } diff --git a/src/hooks/useDataTransferJobs.tsx b/src/hooks/useDataTransferJobs.tsx new file mode 100644 index 000000000..5414dae28 --- /dev/null +++ b/src/hooks/useDataTransferJobs.tsx @@ -0,0 +1,60 @@ +import { getDataTransferJobs } from "Common/dataAccess/dataTransfers"; +import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types"; +import create, { UseStore } from "zustand"; + +export interface DataTransferJobsState { + dataTransferJobs: DataTransferJobGetResults[]; + pollingDataTransferJobs: Set; + setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => void; + setPollingDataTransferJobs: (pollingDataTransferJobs: Set) => void; +} + +type DataTransferJobStore = UseStore; + +export const useDataTransferJobs: DataTransferJobStore = create((set) => ({ + dataTransferJobs: [], + pollingDataTransferJobs: new Set(), + setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => set({ dataTransferJobs }), + setPollingDataTransferJobs: (pollingDataTransferJobs: Set) => set({ pollingDataTransferJobs }), +})); + +export const refreshDataTransferJobs = async ( + subscriptionId: string, + resourceGroup: string, + accountName: string, +): Promise => { + const dataTransferJobs: DataTransferJobGetResults[] = await getDataTransferJobs( + subscriptionId, + resourceGroup, + accountName, + ); + const jobRegex = /^Portal_(.+)_(\d{10,})$/; + const sortedJobs: DataTransferJobGetResults[] = dataTransferJobs?.sort( + (a, b) => + new Date(b?.properties?.lastUpdatedUtcTime).getTime() - new Date(a?.properties?.lastUpdatedUtcTime).getTime(), + ); + const filteredJobs = sortedJobs.filter((job) => jobRegex.test(job?.properties?.jobName)); + useDataTransferJobs.getState().setDataTransferJobs(filteredJobs); + return filteredJobs; +}; + +export const updateDataTransferJob = (updateJob: DataTransferJobGetResults) => { + const updatedDataTransferJobs = useDataTransferJobs + .getState() + .dataTransferJobs.map((job: DataTransferJobGetResults) => + job?.properties?.jobName === updateJob?.properties?.jobName ? updateJob : job, + ); + useDataTransferJobs.getState().setDataTransferJobs(updatedDataTransferJobs); +}; + +export const addToPolling = (addJob: string) => { + const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs; + pollingJobs.add(addJob); + useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs); +}; + +export const removeFromPolling = (removeJob: string) => { + const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs; + pollingJobs.delete(removeJob); + useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs); +}; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 6bbe37583..3a6061acd 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -3,10 +3,10 @@ import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContrac import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract"; import Explorer from "Explorer/Explorer"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; +import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { ReactTabKind, useTabs } from "hooks/useTabs"; import { useEffect, useState } from "react"; @@ -34,10 +34,9 @@ import { getDatabaseAccountPropertiesFromMetadata, } from "../Platform/Hosted/HostedUtils"; import { extractFeatures } from "../Platform/Hosted/extractFeatures"; -import { CollectionCreation } from "../Shared/Constants"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; -import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; +import { acquireTokenWithMsal, getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; @@ -91,6 +90,7 @@ async function configureFabric(): Promise { // These are the versions of Fabric that Data Explorer supports. const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION]; + let firstContainerOpened = false; let explorer: Explorer; return new Promise((resolve) => { window.addEventListener( @@ -122,7 +122,10 @@ async function configureFabric(): Promise { await scheduleRefreshDatabaseResourceToken(true); resolve(explorer); await explorer.refreshAllDatabases(); - openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + if (userContext.fabricContext.isVisible && !firstContainerOpened) { + firstContainerOpened = true; + openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + } break; } case "newContainer": @@ -133,8 +136,16 @@ async function configureFabric(): Promise { handleCachedDataMessage(data); break; } - case "setToolbarStatus": { - useCommandBar.getState().setIsHidden(data.message.visible === false); + case "explorerVisible": { + userContext.fabricContext.isVisible = data.message.visible; + if ( + userContext.fabricContext.isVisible && + !firstContainerOpened && + userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined + ) { + firstContainerOpened = true; + openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); + } break; } default: @@ -245,16 +256,19 @@ async function configureHostedWithAAD(config: AAD): Promise { let keys: DatabaseAccountListKeysResult = {}; if (account.properties?.documentEndpoint) { const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); - const msalInstance = getMsalInstance(); + const msalInstance = await getMsalInstance(); const cachedAccount = msalInstance.getAllAccounts()?.[0]; msalInstance.setActiveAccount(cachedAccount); const cachedTenantId = localStorage.getItem("cachedTenantId"); - const aadTokenResponse = await msalInstance.acquireTokenSilent({ - forceRefresh: true, - scopes: [hrefEndpoint], - authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`, - }); - aadToken = aadTokenResponse.accessToken; + try { + aadToken = await acquireTokenWithMsal(msalInstance, { + forceRefresh: true, + scopes: [hrefEndpoint], + authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`, + }); + } catch (authError) { + logConsoleError("Failed to acquire authorization token: " + authError); + } } try { if (!account.properties.disableLocalAuth) { @@ -325,11 +339,13 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer { return explorer; } -function createExplorerFabric(params: { connectionId: string }): Explorer { +function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer { updateUserContext({ fabricContext: { connectionId: params.connectionId, databaseConnectionInfo: undefined, + isReadOnly: true, + isVisible: params.isVisible ?? true, }, authType: AuthType.ConnectionString, databaseAccount: { @@ -480,6 +496,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { updateConfigContext({ BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT, ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), + MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint, + CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint, + PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint, }); updateUserContext({ @@ -492,9 +511,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { quotaId: inputs.quotaId, portalEnv: inputs.serverId as PortalEnv, hasWriteAccess: inputs.hasWriteAccess ?? true, - addCollectionFlight: inputs.addCollectionDefaultFlight || CollectionCreation.DefaultAddCollectionDefaultFlight, collectionCreationDefaults: inputs.defaultCollectionThroughput, isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription, + feedbackPolicies: inputs.feedbackPolicies, }); if (inputs.isPostgresAccount) { diff --git a/src/hooks/useQueryCopilot.ts b/src/hooks/useQueryCopilot.ts index 1d9685b6f..23e5a0aa6 100644 --- a/src/hooks/useQueryCopilot.ts +++ b/src/hooks/useQueryCopilot.ts @@ -29,6 +29,7 @@ export interface QueryCopilotState { queryResults: QueryResults | undefined; errorMessage: string; isSamplePromptsOpen: boolean; + showPromptTeachingBubble: boolean; showDeletePopup: boolean; showFeedbackBar: boolean; showCopyPopup: boolean; @@ -71,6 +72,7 @@ export interface QueryCopilotState { setQueryResults: (queryResults: QueryResults | undefined) => void; setErrorMessage: (errorMessage: string) => void; setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void; + setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => void; setShowDeletePopup: (showDeletePopup: boolean) => void; setShowFeedbackBar: (showFeedbackBar: boolean) => void; setshowCopyPopup: (showCopyPopup: boolean) => void; @@ -93,7 +95,7 @@ export interface QueryCopilotState { resetQueryCopilotStates: () => void; } -type QueryCopilotStore = UseStore; +type QueryCopilotStore = UseStore>; export const useQueryCopilot: QueryCopilotStore = create((set) => ({ copilotEnabled: false, diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index 136e0ce2c..982768afa 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -1,3 +1,4 @@ +import { clamp } from "@fluentui/react"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels"; @@ -29,6 +30,11 @@ export interface TabsState { setQueryCopilotTabInitialInput: (input: string) => void; setIsTabExecuting: (state: boolean) => void; setIsQueryErrorThrown: (state: boolean) => void; + getCurrentTabIndex: () => number; + selectTabByIndex: (index: number) => void; + selectLeftTab: () => void; + selectRightTab: () => void; + closeActiveTab: () => void; } export enum ReactTabKind { @@ -105,7 +111,7 @@ export const useTabs: UseStore = create((set, get) => ({ return true; }); if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) { - set({ activeTab: undefined, activeReactTab: ReactTabKind.Home }); + set({ activeTab: undefined, activeReactTab: undefined }); } if (tab.tabId === activeTab.tabId && tabIndex !== -1) { @@ -143,7 +149,7 @@ export const useTabs: UseStore = create((set, get) => ({ }); if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) { - set({ activeTab: undefined, activeReactTab: ReactTabKind.Home }); + set({ activeTab: undefined, activeReactTab: undefined }); } } }, @@ -175,4 +181,44 @@ export const useTabs: UseStore = create((set, get) => ({ setIsQueryErrorThrown: (state: boolean) => { set({ isQueryErrorThrown: state }); }, + getCurrentTabIndex: () => { + const state = get(); + if (state.activeReactTab !== undefined) { + return state.openedReactTabs.indexOf(state.activeReactTab); + } else if (state.activeTab !== undefined) { + const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab); + if (nonReactTabIndex !== -1) { + return state.openedReactTabs.length + nonReactTabIndex; + } + } + + return -1; + }, + selectTabByIndex: (index: number) => { + const state = get(); + const totalTabCount = state.openedReactTabs.length + state.openedTabs.length; + const clampedIndex = clamp(index, totalTabCount - 1, 0); + + if (clampedIndex < state.openedReactTabs.length) { + set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] }); + } else { + set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined }); + } + }, + selectLeftTab: () => { + const state = get(); + state.selectTabByIndex(state.getCurrentTabIndex() - 1); + }, + selectRightTab: () => { + const state = get(); + state.selectTabByIndex(state.getCurrentTabIndex() + 1); + }, + closeActiveTab: () => { + const state = get(); + if (state.activeReactTab !== undefined) { + state.closeReactTab(state.activeReactTab); + } else if (state.activeTab !== undefined) { + state.closeTab(state.activeTab); + } + }, })); diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 209c34eec..80d5df41d 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Cassandra keyspace and table CRUD", async () => { const keyspaceId = generateUniqueName("keyspace"); const tableId = generateUniqueName("table"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`); await page.waitForSelector("iframe"); const explorer = await waitForExplorer(); @@ -20,11 +23,11 @@ test("Cassandra keyspace and table CRUD", async () => { await explorer.fill('[aria-label="addCollection-table Id Create table"]', tableId); await explorer.click("#sidePanelOkButton"); await explorer.click(`.nodeItem >> text=${keyspaceId}`); - await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', keyspaceId); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index 342c12d4b..e7f288da5 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and graph @@ -21,11 +24,11 @@ test("Graph CRUD", async () => { await explorer.click(`.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and graph - await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Graph")'); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index d6a9271ed..baafefbbe 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection @@ -32,11 +35,11 @@ test("Mongo CRUD", async () => { await explorer.click('[aria-label="Delete index Button"]'); await explorer.click('[data-test="Save"]'); // Delete database and collection - await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index ac6dbc39c..c71f9d0cc 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection @@ -21,11 +24,11 @@ test("Mongo CRUD", async () => { explorer.click(`.nodeItem >> text=${databaseId}`); explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and collection - explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/selfServe/selfServeExample.spec.ts b/test/selfServe/selfServeExample.spec.ts index 7e10c1ce2..3678f5b35 100644 --- a/test/selfServe/selfServeExample.spec.ts +++ b/test/selfServe/selfServeExample.spec.ts @@ -1,5 +1,10 @@ +import { getAzureCLICredentialsToken } from "../utils/shared"; + test("Self Serve", async () => { - await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); + + await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`); const handle = await page.waitForSelector("iframe"); const frame = await handle.contentFrame(); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 975687aaa..a1aacfd42 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`); const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); @@ -18,11 +21,11 @@ test("SQL CRUD", async () => { await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); await explorer.click(`.nodeItem >> text=${databaseId}`); - await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Container")'); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); - await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${databaseId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId); diff --git a/test/sql/resourceToken.spec.ts b/test/sql/resourceToken.spec.ts index f2e9d94ef..18228c7ed 100644 --- a/test/sql/resourceToken.spec.ts +++ b/test/sql/resourceToken.spec.ts @@ -1,19 +1,15 @@ import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosClient, PermissionMode } from "@azure/cosmos"; -import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentials } from "../utils/shared"; jest.setTimeout(120000); -const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4"; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? ""; const resourceGroupName = "runners"; test("Resource token", async () => { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index 932127bb1..98687d60f 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,15 +1,17 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`); const explorer = await waitForExplorer(); await page.waitForSelector('text="Querying databases"', { state: "detached" }); @@ -17,7 +19,7 @@ test("Tables CRUD", async () => { await explorer.fill('[aria-label="Table id, Example Table1"]', tableId); await explorer.click("#sidePanelOkButton"); await explorer.click(`[data-test="TablesDB"]`); - await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More options"]`); await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index 540de9768..4dbeb86b6 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import { ClientSecretCredential } from "@azure/identity"; import "../../less/hostedexplorer.less"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { updateUserContext } from "../../src/UserContext"; @@ -11,29 +10,13 @@ const urlSearchParams = new URLSearchParams(window.location.search); const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us"; const selfServeType = urlSearchParams.get("selfServeType") || "example"; const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; - -if (!process.env.AZURE_CLIENT_SECRET) { - throw new Error( - "process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server", - ); -} - -// Azure SDK clients accept the credential as a parameter -const credentials = new ClientSecretCredential( - process.env.AZURE_TENANT_ID, - process.env.AZURE_CLIENT_ID, - process.env.AZURE_CLIENT_SECRET, - { - authorityHost: "https://localhost:1234", - }, -); +const token = urlSearchParams.get("token"); console.log("Resource Group:", resourceGroup); console.log("Subcription: ", subscriptionId); console.log("Account Name: ", accountName); const initTestExplorer = async (): Promise => { - const { token } = await credentials.getToken("https://management.azure.com//.default"); updateUserContext({ authorizationToken: `bearer ${token}`, }); @@ -52,9 +35,11 @@ const initTestExplorer = async (): Promise => { dnsSuffix: "documents.azure.com", serverId: "prod1", extensionEndpoint: "/proxy", + portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com", + mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com", + cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com", subscriptionType: 3, quotaId: "Internal_2014-09-01", - addCollectionDefaultFlight: "2", isTryCosmosDBSubscription: false, masterKey: keys.primaryMasterKey, loadDatabaseAccountTimestamp: 1604663109836, diff --git a/test/utils/shared.ts b/test/utils/shared.ts index 118736129..59ef0994c 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -1,3 +1,4 @@ +import { AzureCliCredentials } from "@azure/ms-rest-nodeauth"; import crypto from "crypto"; export function generateUniqueName(baseName = "", length = 4): string { @@ -7,3 +8,13 @@ export function generateUniqueName(baseName = "", length = 4): string { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string { return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; } + +export async function getAzureCLICredentials(): Promise { + return await AzureCliCredentials.create(); +} + +export async function getAzureCLICredentialsToken(): Promise { + const credentials = await getAzureCLICredentials(); + const token = (await credentials.getToken()).accessToken; + return token; +} diff --git a/tsconfig.strict.json b/tsconfig.strict.json index be0195655..969c39d01 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -112,6 +112,7 @@ "./src/Utils/BlobUtils.ts", "./src/Utils/CapabilityUtils.ts", "./src/Utils/CloudUtils.ts", + "./src/Utils/EndpointUtils.ts", "./src/Utils/GitHubUtils.test.ts", "./src/Utils/GitHubUtils.ts", "./src/Utils/MessageValidation.test.ts", diff --git a/utils/armClientGenerator/generator.ts b/utils/armClientGenerator/generator.ts index 01a7e2e72..1a50e9082 100644 --- a/utils/armClientGenerator/generator.ts +++ b/utils/armClientGenerator/generator.ts @@ -16,13 +16,13 @@ Results of this file should be checked into the repo. */ // CHANGE THESE VALUES TO GENERATE NEW CLIENTS -const version = "2023-09-15-preview"; +const version = "2024-02-15-preview"; /* The following are legal options for resourceName but you generally will only use cosmos-db: "cosmos-db" | "managedCassandra" | "mongorbac" | "notebook" | "privateEndpointConnection" | "privateLinkResources" | -"rbac" | "restorable" | "services" +"rbac" | "restorable" | "services" | "dataTransferService" */ const githubResourceName = "cosmos-db"; -const deResourceName = "cosmos"; +const deResourceName = "cosmos-db"; const schemaURL = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/${version}/${githubResourceName}.json`; const outputDir = path.join(__dirname, `../../src/Utils/arm/generatedClients/${deResourceName}`); @@ -117,9 +117,9 @@ const propertyToType = (property: Property, prop: string, required: boolean) => if (property.allOf) { outputTypes.push(` /* ${property.description || "undocumented"} */ - ${property.readOnly ? "readonly " : ""}${prop}${ - required ? "" : "?" - }: ${property.allOf.map((allof: { $ref: string }) => refToType(allof.$ref)).join(" & ")}`); + ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.allOf + .map((allof: { $ref: string }) => refToType(allof.$ref)) + .join(" & ")}`); } else if (property.$ref) { const type = refToType(property.$ref); outputTypes.push(` @@ -142,8 +142,8 @@ const propertyToType = (property: Property, prop: string, required: boolean) => outputTypes.push(` /* ${property.description || "undocumented"} */ ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${property.enum - .map((v: string) => `"${v}"`) - .join(" | ")} + .map((v: string) => `"${v}"`) + .join(" | ")} `); } else { if (property.type === undefined) { @@ -153,8 +153,8 @@ const propertyToType = (property: Property, prop: string, required: boolean) => outputTypes.push(` /* ${property.description || "undocumented"} */ ${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${ - propertyMap[property.type] ? propertyMap[property.type] : property.type - }`); + propertyMap[property.type] ? propertyMap[property.type] : property.type + }`); } } }; @@ -247,7 +247,7 @@ async function main() { const operation = schema.paths[path][method]; const [, methodName] = operation.operationId.split("_"); const bodyParameter = operation.parameters.find( - (parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true + (parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true, ); outputClient.push(` /* ${operation.description || "undocumented"} */ @@ -259,8 +259,8 @@ async function main() { ) : Promise<${responseType(operation, "Types")}> { const path = \`${path.replace(/{/g, "${")}\` return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "${method.toLocaleUpperCase()}", apiVersion, ${ - bodyParameter ? "body" : "" - } }) + bodyParameter ? "body" : "" + } }) } `); } diff --git a/utils/cleanupDBs.js b/utils/cleanupDBs.js index 72fcfbafd..b2bbf0be8 100644 --- a/utils/cleanupDBs.js +++ b/utils/cleanupDBs.js @@ -2,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const ms = require("ms"); -const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; const resourceGroupName = "runners"; const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); @@ -19,7 +16,7 @@ function friendlyTime(date) { } async function main() { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await msRestNodeAuth.AzureCliCredentials.create(); const client = new CosmosDBManagementClient(credentials, subscriptionId); const accounts = await client.databaseAccounts.list(resourceGroupName); for (const account of accounts) { @@ -38,7 +35,7 @@ async function main() { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( resourceGroupName, - account.name + account.name, ); for (const database of cassandraDatabases) { const timestamp = Number(database.resource._ts) * 1000; diff --git a/web.config b/web.config index 9d9ff2619..4a967e52a 100644 --- a/web.config +++ b/web.config @@ -30,7 +30,7 @@ - +