Merge branch 'master' into users/languye/upgrade-fabric-to-v3
This commit is contained in:
commit
0a8e3736dd
|
@ -145,4 +145,5 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
||||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTree.tsx
|
src/Explorer/Tree/ResourceTree.tsx
|
||||||
|
src/Utils/EndpointUtils.ts
|
||||||
src/Utils/PriorityBasedExecutionUtils.ts
|
src/Utils/PriorityBasedExecutionUtils.ts
|
|
@ -8,6 +8,9 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
codemetrics:
|
codemetrics:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -134,7 +137,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -145,11 +148,18 @@ jobs:
|
||||||
- ./test/mongo/container.spec.ts
|
- ./test/mongo/container.spec.ts
|
||||||
- ./test/mongo/container32.spec.ts
|
- ./test/mongo/container32.spec.ts
|
||||||
- ./test/selfServe/selfServeExample.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/sql/resourceToken.spec.ts
|
||||||
- ./test/tables/container.spec.ts
|
- ./test/tables/container.spec.ts
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -9,6 +9,10 @@ on:
|
||||||
# Once every hour
|
# Once every hour
|
||||||
- cron: "0 15 * * *"
|
- 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
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
jobs:
|
jobs:
|
||||||
# This workflow contains a single job called "build"
|
# This workflow contains a single job called "build"
|
||||||
|
@ -16,10 +20,17 @@ jobs:
|
||||||
name: "Cleanup Test Database Accounts"
|
name: "Cleanup Test Database Accounts"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- 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
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true,
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.organizeImports": true
|
"source.organizeImports": "explicit"
|
||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled" : true,
|
"isTerminalEnabled": true,
|
||||||
"isPhoenixEnabled" : true
|
"isPhoenixEnabled": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.31299 1.26164C7.69849 0.897163 8.30151 0.897163 8.68701 1.26164L13.5305 5.84098C13.8302 6.12431 14 6.51853 14 6.93094V12.5002C14 13.3286 13.3284 14.0002 12.5 14.0002H10.5C9.67157 14.0002 9 13.3286 9 12.5002V10.0002C9 9.72407 8.77614 9.50021 8.5 9.50021H7.5C7.22386 9.50021 7 9.72407 7 10.0002V12.5002C7 13.3286 6.32843 14.0002 5.5 14.0002H3.5C2.67157 14.0002 2 13.3286 2 12.5002V6.93094C2 6.51853 2.1698 6.12431 2.46948 5.84098L7.31299 1.26164ZM8 1.98828L3.15649 6.56762C3.0566 6.66207 3 6.79347 3 6.93094V12.5002C3 12.7763 3.22386 13.0002 3.5 13.0002H5.5C5.77614 13.0002 6 12.7763 6 12.5002V10.0002C6 9.17179 6.67157 8.50022 7.5 8.50022H8.5C9.32843 8.50022 10 9.17179 10 10.0002V12.5002C10 12.7763 10.2239 13.0002 10.5 13.0002H12.5C12.7761 13.0002 13 12.7763 13 12.5002V6.93094C13 6.79347 12.9434 6.66207 12.8435 6.56762L8 1.98828Z" fill="#0078D4" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 968 B |
|
@ -76,6 +76,10 @@ module.exports = {
|
||||||
"^dnd-core$": "dnd-core/dist/cjs",
|
"^dnd-core$": "dnd-core/dist/cjs",
|
||||||
"^react-dnd$": "react-dnd/dist/cjs",
|
"^react-dnd$": "react-dnd/dist/cjs",
|
||||||
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
|
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
|
||||||
|
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.min.js",
|
||||||
|
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
|
||||||
|
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
|
||||||
|
"d3-zoom": "<rootDir>/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
|
// 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
|
// The test environment that will be used for testing
|
||||||
// testEnvironment: "jest-environment-jsdom",
|
// testEnvironment: "jest-environment-jsdom",
|
||||||
|
|
||||||
modulePaths: ["node_modules", "<rootDir>/src"],
|
modulePaths: ["node_modules", "<rootDir>/src"],
|
||||||
|
|
||||||
// Options that will be passed to the testEnvironment
|
// Options that will be passed to the testEnvironment
|
||||||
|
|
|
@ -163,6 +163,7 @@
|
||||||
/**********************************************************************************/
|
/**********************************************************************************/
|
||||||
|
|
||||||
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
@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;
|
@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;
|
@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;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-color: @InfoPointerColor transparent;
|
border-color: @InfoPointerColor transparent;
|
||||||
|
}
|
||||||
|
/*********************************************************************************************************
|
||||||
|
Screen Reader Only
|
||||||
|
**********************************************************************************************************/
|
||||||
|
.screenReaderOnly {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
}
|
}
|
|
@ -2296,6 +2296,17 @@ a:link {
|
||||||
display: none !important;
|
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 {
|
td a {
|
||||||
color: #393939;
|
color: #393939;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,29 +25,30 @@ a:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: @FabricBoxBorderRadius;
|
border-radius: 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 4px;
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #fafafa
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
background-color: #fafafa
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-bottom: none;
|
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||||
border-radius: @FabricBoxBorderRadius;
|
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
@ -162,9 +163,10 @@ a:focus {
|
||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: @FabricBoxBorderRadius;
|
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
|
margin-top: 0px;
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
|
@ -5,10 +5,10 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@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/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.2.1",
|
"@azure/identity": "1.5.2",
|
||||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
|
@ -46,6 +46,7 @@
|
||||||
"@types/lodash": "4.14.171",
|
"@types/lodash": "4.14.171",
|
||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
|
"@xmldom/xmldom": "0.7.13",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
"canvas": "file:./canvas",
|
"canvas": "file:./canvas",
|
||||||
|
@ -54,7 +55,7 @@
|
||||||
"copy-webpack-plugin": "11.0.0",
|
"copy-webpack-plugin": "11.0.0",
|
||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
"css-element-queries": "1.1.1",
|
"css-element-queries": "1.1.1",
|
||||||
"d3": "6.1.1",
|
"d3": "7.8.5",
|
||||||
"datatables.net-colreorder-dt": "1.7.0",
|
"datatables.net-colreorder-dt": "1.7.0",
|
||||||
"datatables.net-dt": "1.13.8",
|
"datatables.net-dt": "1.13.8",
|
||||||
"date-fns": "1.29.0",
|
"date-fns": "1.29.0",
|
||||||
|
@ -69,17 +70,19 @@
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.0.1",
|
||||||
"i18next-http-backend": "1.0.23",
|
"i18next-http-backend": "1.0.23",
|
||||||
"iframe-resizer-react": "1.1.0",
|
"iframe-resizer-react": "1.1.0",
|
||||||
|
"immer": "9.0.6",
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
"is-ci": "2.0.0",
|
"is-ci": "2.0.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-typeahead": "2.11.1",
|
"jquery-typeahead": "2.11.1",
|
||||||
"jquery-ui-dist": "1.13.2",
|
"jquery-ui-dist": "1.13.2",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
|
"loader-utils": "2.0.3",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"patch-package": "8.0.0",
|
|
||||||
"p-retry": "4.6.2",
|
"p-retry": "4.6.2",
|
||||||
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
"q": "1.5.1",
|
"q": "1.5.1",
|
||||||
|
@ -98,10 +101,12 @@
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rx-jupyter": "5.5.12",
|
"rx-jupyter": "5.5.12",
|
||||||
"sanitize-html": "2.3.3",
|
"sanitize-html": "2.3.3",
|
||||||
|
"shell-quote": "1.7.3",
|
||||||
"styled-components": "5.0.1",
|
"styled-components": "5.0.1",
|
||||||
"swr": "0.4.0",
|
"swr": "0.4.0",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
"underscore": "1.9.1",
|
"tinykeys": "2.1.0",
|
||||||
|
"underscore": "1.12.1",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
},
|
},
|
||||||
|
@ -170,25 +175,25 @@
|
||||||
"less-vars-loader": "1.1.0",
|
"less-vars-loader": "1.1.0",
|
||||||
"mini-css-extract-plugin": "2.1.0",
|
"mini-css-extract-plugin": "2.1.0",
|
||||||
"monaco-editor-webpack-plugin": "7.1.0",
|
"monaco-editor-webpack-plugin": "7.1.0",
|
||||||
"node-fetch": "2.6.1",
|
"node-fetch": "2.6.7",
|
||||||
"playwright": "1.13.0",
|
"playwright": "1.13.0",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"querystring-es3": "0.2.1",
|
"querystring-es3": "0.2.1",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"react-dev-utils": "11.0.4",
|
"react-dev-utils": "12.0.1",
|
||||||
"rimraf": "3.0.0",
|
"rimraf": "3.0.0",
|
||||||
"sinon": "3.2.1",
|
"sinon": "3.2.1",
|
||||||
"style-loader": "0.23.0",
|
"style-loader": "0.23.0",
|
||||||
"ts-loader": "9.2.4",
|
"ts-loader": "9.2.4",
|
||||||
"typedoc": "0.21.5",
|
"typedoc": "0.22.15",
|
||||||
"typescript": "4.3.5",
|
"typescript": "4.3.5",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"wait-on": "4.0.2",
|
"wait-on": "4.0.2",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.88.2",
|
||||||
"webpack-bundle-analyzer": "4.9.1",
|
"webpack-bundle-analyzer": "4.9.1",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "4.15.1"
|
"webpack-dev-server": "4.15.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
|
@ -238,4 +243,4 @@
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,12 @@ export class CapabilityNames {
|
||||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||||
public static readonly EnableMongo: string = "EnableMongo";
|
public static readonly EnableMongo: string = "EnableMongo";
|
||||||
public static readonly EnableServerless: string = "EnableServerless";
|
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
|
// flight names returned from the portal are always lowercase
|
||||||
|
@ -124,7 +130,36 @@ export enum MongoBackendEndpointType {
|
||||||
remote,
|
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 {
|
export class CassandraBackend {
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/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";
|
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 {
|
export class Queries {
|
||||||
public static CustomPageOption: string = "custom";
|
public static CustomPageOption: string = "custom";
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
|
@ -211,6 +257,10 @@ export class HttpHeaders {
|
||||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ContentType {
|
||||||
|
public static applicationJson: string = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiType {
|
export class ApiType {
|
||||||
// Mapped to hexadecimal values in the backend
|
// Mapped to hexadecimal values in the backend
|
||||||
public static readonly MongoDB: number = 1;
|
public static readonly MongoDB: number = 1;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
|
||||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||||
import { AuthorizationToken, FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
|
@ -51,15 +50,23 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
case Cosmos.ResourceType.offer:
|
case Cosmos.ResourceType.offer:
|
||||||
case Cosmos.ResourceType.user:
|
case Cosmos.ResourceType.user:
|
||||||
case Cosmos.ResourceType.permission:
|
case Cosmos.ResourceType.permission:
|
||||||
// User master tokens
|
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||||
FabricMessageTypes.GetAuthorizationToken,
|
// (which is a valid token, but won't work for these operations).
|
||||||
[requestInfo],
|
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.connectionId,
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||||
);
|
|
||||||
console.log("Response from Fabric: ", authorizationToken);
|
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
// User master tokens
|
||||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
||||||
|
FabricMessageTypes.GetAuthorizationToken,
|
||||||
|
[requestInfo],
|
||||||
|
userContext.fabricContext.connectionId,
|
||||||
|
);
|
||||||
|
console.log("Response from Fabric: ", authorizationToken);
|
||||||
|
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||||
|
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||||
|
***********************************************************************************************/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { QueryOperationOptions } from "@azure/cosmos";
|
||||||
import { QueryResults } from "../Contracts/ViewModels";
|
import { QueryResults } from "../Contracts/ViewModels";
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
|
@ -10,13 +11,17 @@ interface QueryResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MinimalQueryIterator {
|
export interface MinimalQueryIterator {
|
||||||
fetchNext: () => Promise<QueryResponse>;
|
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick<QueryIterator<any>, "fetchNext">;
|
// Pick<QueryIterator<any>, "fetchNext">;
|
||||||
|
|
||||||
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
export function nextPage(
|
||||||
return documentsIterator.fetchNext().then((response) => {
|
documentsIterator: MinimalQueryIterator,
|
||||||
|
firstItemIndex: number,
|
||||||
|
queryOperationOptions?: QueryOperationOptions,
|
||||||
|
): Promise<QueryResults> {
|
||||||
|
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
||||||
const documents = response.resources;
|
const documents = response.resources;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
|
import {
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
|
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
validateEndpoint,
|
||||||
|
} from "Utils/EndpointUtils";
|
||||||
import queryString from "querystring";
|
import queryString from "querystring";
|
||||||
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
@ -10,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
|
@ -62,6 +66,73 @@ export function queryDocuments(
|
||||||
isResourceList: boolean,
|
isResourceList: boolean,
|
||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
|
): Promise<QueryResponse> {
|
||||||
|
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<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
@ -122,6 +193,54 @@ export function readDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
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<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
@ -169,6 +288,51 @@ export function createDocument(
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
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<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
@ -208,6 +372,56 @@ export function updateDocument(
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
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<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
@ -237,7 +451,7 @@ export function updateDocument(
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
...authHeaders(),
|
...authHeaders(),
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -250,6 +464,53 @@ export function updateDocument(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
|
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<void> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
|
@ -277,7 +538,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
...authHeaders(),
|
...authHeaders(),
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -291,6 +552,52 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
|
): Promise<DataModels.Collection> {
|
||||||
|
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<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
|
@ -334,13 +641,20 @@ export function createMongoCollectionWithProxy(
|
||||||
return await errorHandling(response, "creating collection", mongoParams);
|
return await errorHandling(response, "creating collection", mongoParams);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||||
const endpoint =
|
let endpoint;
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
if (useMongoProxyEndpoint(feature)) {
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||||
? userContext.features.mongoProxyEndpoint
|
} else {
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
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);
|
return getEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
|
@ -349,11 +663,37 @@ export function getEndpoint(endpoint: string): string {
|
||||||
let url = endpoint + "/api/mongo/explorer";
|
let url = endpoint + "/api/mongo/explorer";
|
||||||
|
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
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;
|
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
|
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
||||||
// It causes problems for TypeScript understanding the types
|
// It causes problems for TypeScript understanding the types
|
||||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||||
|
|
|
@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={EditIcon}
|
src={EditIcon}
|
||||||
alt="editEntity"
|
alt={`Edit ${entityProperty} entity`}
|
||||||
onClick={onEditEntity}
|
onClick={onEditEntity}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
|
@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={DeleteIcon}
|
src={DeleteIcon}
|
||||||
alt="delete entity"
|
alt={`Delete ${entityProperty} entity`}
|
||||||
id="deleteEntity"
|
id="deleteEntity"
|
||||||
onClick={onDeleteEntity}
|
onClick={onDeleteEntity}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||||
Object {
|
Object {
|
||||||
|
"disableNonStreamingOrderByQuery": true,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
"maxDegreeOfParallelism": 0,
|
"maxDegreeOfParallelism": 0,
|
||||||
|
@ -12,6 +13,7 @@ Object {
|
||||||
|
|
||||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||||
Object {
|
Object {
|
||||||
|
"disableNonStreamingOrderByQuery": true,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
"maxDegreeOfParallelism": 17,
|
"maxDegreeOfParallelism": 17,
|
||||||
|
|
|
@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||||
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||||
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
||||||
|
@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||||
if (params.uniqueKeyPolicy) {
|
if (params.uniqueKeyPolicy) {
|
||||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
||||||
}
|
}
|
||||||
|
if (params.vectorEmbeddingPolicy) {
|
||||||
|
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { ApiType, userContext } from "UserContext";
|
||||||
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
|
import {
|
||||||
|
cancel,
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
listByDatabaseAccount,
|
||||||
|
} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
|
import {
|
||||||
|
CosmosCassandraDataTransferDataSourceSink,
|
||||||
|
CosmosMongoDataTransferDataSourceSink,
|
||||||
|
CosmosSqlDataTransferDataSourceSink,
|
||||||
|
CreateJobRequest,
|
||||||
|
DataTransferJobFeedResults,
|
||||||
|
DataTransferJobGetResults,
|
||||||
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
|
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||||
|
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
||||||
|
|
||||||
|
export interface DataTransferParams {
|
||||||
|
jobName: string;
|
||||||
|
apiType: ApiType;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroupName: string;
|
||||||
|
accountName: string;
|
||||||
|
sourceDatabaseName: string;
|
||||||
|
sourceCollectionName: string;
|
||||||
|
targetDatabaseName: string;
|
||||||
|
targetCollectionName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDataTransferJobs = async (
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
): Promise<DataTransferJobGetResults[]> => {
|
||||||
|
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<DataTransferJobGetResults> => {
|
||||||
|
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<unknown> => {
|
||||||
|
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<DataTransferJobGetResults> => {
|
||||||
|
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<void> => {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Queries } from "../Constants";
|
import { Queries } from "../Constants";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
|
@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||||
Queries.itemsPerPage;
|
Queries.itemsPerPage;
|
||||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||||
|
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { QueryOperationOptions } from "@azure/cosmos";
|
||||||
import { QueryResults } from "../../Contracts/ViewModels";
|
import { QueryResults } from "../../Contracts/ViewModels";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { getEntityName } from "../DocumentUtility";
|
import { getEntityName } from "../DocumentUtility";
|
||||||
|
@ -8,12 +9,13 @@ export const queryDocumentsPage = async (
|
||||||
resourceName: string,
|
resourceName: string,
|
||||||
documentsIterator: MinimalQueryIterator,
|
documentsIterator: MinimalQueryIterator,
|
||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
|
queryOperationOptions?: QueryOperationOptions,
|
||||||
): Promise<QueryResults> => {
|
): Promise<QueryResults> => {
|
||||||
const entityName = getEntityName();
|
const entityName = getEntityName();
|
||||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
|
||||||
const itemCount = (result.documents && result.documents.length) || 0;
|
const itemCount = (result.documents && result.documents.length) || 0;
|
||||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
import { JunoEndpoints } from "Common/Constants";
|
import {
|
||||||
|
BackendApi,
|
||||||
|
CassandraProxyEndpoints,
|
||||||
|
JunoEndpoints,
|
||||||
|
MongoProxyEndpoints,
|
||||||
|
PortalBackendEndpoints,
|
||||||
|
} from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
|
allowedCassandraProxyEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointValidation";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Portal = "Portal",
|
Portal = "Portal",
|
||||||
|
@ -37,7 +45,15 @@ export interface ConfigContext {
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
|
PORTAL_BACKEND_ENDPOINT?: string;
|
||||||
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
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;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
|
@ -67,6 +83,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||||
|
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
|
@ -82,6 +99,22 @@ let configContext: Readonly<ConfigContext> = {
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
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,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
|
@ -127,10 +160,18 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
||||||
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
||||||
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||||
delete newContext.JUNO_ENDPOINT;
|
delete newContext.JUNO_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
@ -148,10 +189,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||||
|
|
||||||
// Injected for local development. These will be removed in the production bundle by webpack
|
// Injected for local development. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
const port: string = process.env.PORT || "1234";
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://localhost:" + port,
|
|
||||||
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
|
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||||
|
|
||||||
export interface ArmEntity {
|
export interface ArmEntity {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties {
|
||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
capacity?: { totalThroughputLimit: number };
|
capacity?: { totalThroughputLimit: number };
|
||||||
|
capacityMode?: CapacityMode;
|
||||||
locations?: DatabaseAccountResponseLocation[];
|
locations?: DatabaseAccountResponseLocation[];
|
||||||
postgresqlEndpoint?: string;
|
postgresqlEndpoint?: string;
|
||||||
publicNetworkAccess?: string;
|
publicNetworkAccess?: string;
|
||||||
|
@ -157,8 +158,10 @@ export interface Collection extends Resource {
|
||||||
changeFeedPolicy?: ChangeFeedPolicy;
|
changeFeedPolicy?: ChangeFeedPolicy;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
|
computedProperties?: ComputedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionsWithPagination {
|
export interface CollectionsWithPagination {
|
||||||
|
@ -193,10 +196,23 @@ export interface IndexingPolicy {
|
||||||
indexingMode: "consistent" | "lazy" | "none";
|
indexingMode: "consistent" | "lazy" | "none";
|
||||||
includedPaths: any;
|
includedPaths: any;
|
||||||
excludedPaths: any;
|
excludedPaths: any;
|
||||||
compositeIndexes?: any;
|
compositeIndexes?: any[];
|
||||||
spatialIndexes?: 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 {
|
export interface PartitionKey {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
kind: "Hash" | "Range" | "MultiHash";
|
kind: "Hash" | "Range" | "MultiHash";
|
||||||
|
@ -325,6 +341,18 @@ export interface CreateCollectionParams {
|
||||||
partitionKey?: PartitionKey;
|
partitionKey?: PartitionKey;
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
createMongoWildcardIndex?: boolean;
|
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 {
|
export interface ReadDatabaseOfferParams {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
|
||||||
import * as ActionContracts from "./ActionContracts";
|
import * as ActionContracts from "./ActionContracts";
|
||||||
import * as Diagnostics from "./Diagnostics";
|
import * as Diagnostics from "./Diagnostics";
|
||||||
|
import { MessageTypes } from "./MessageTypes";
|
||||||
import * as Versions from "./Versions";
|
import * as Versions from "./Versions";
|
||||||
|
|
||||||
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
||||||
|
|
|
@ -53,6 +53,7 @@ export type FabricMessageV2 =
|
||||||
id: string;
|
id: string;
|
||||||
message: {
|
message: {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
isVisible: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
@ -72,7 +73,7 @@ export type FabricMessageV2 =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "setToolbarStatus";
|
type: "explorerVisible";
|
||||||
message: {
|
message: {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -135,6 +135,7 @@ export interface Collection extends CollectionBase {
|
||||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
documentIds: ko.ObservableArray<DocumentId>;
|
documentIds: ko.ObservableArray<DocumentId>;
|
||||||
|
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||||
|
|
||||||
cassandraKeys: CassandraTableKeys;
|
cassandraKeys: CassandraTableKeys;
|
||||||
cassandraSchema: CassandraTableKey[];
|
cassandraSchema: CassandraTableKey[];
|
||||||
|
@ -386,9 +387,11 @@ export interface DataExplorerInputsFrame {
|
||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
extensionEndpoint?: string;
|
extensionEndpoint?: string;
|
||||||
|
portalBackendEndpoint?: string;
|
||||||
|
mongoProxyEndpoint?: string;
|
||||||
|
cassandraProxyEndpoint?: string;
|
||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
quotaId?: string;
|
quotaId?: string;
|
||||||
addCollectionDefaultFlight?: string;
|
|
||||||
isTryCosmosDBSubscription?: boolean;
|
isTryCosmosDBSubscription?: boolean;
|
||||||
loadDatabaseAccountTimestamp?: number;
|
loadDatabaseAccountTimestamp?: number;
|
||||||
sharedThroughputMinimum?: number;
|
sharedThroughputMinimum?: number;
|
||||||
|
@ -406,6 +409,7 @@ export interface DataExplorerInputsFrame {
|
||||||
features?: {
|
features?: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
feedbackPolicies?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelfServeFrameInputs {
|
export interface SelfServeFrameInputs {
|
||||||
|
|
|
@ -26,8 +26,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
this.setState({ isExpanded: !this.state.isExpanded });
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidUpdate(): void {
|
public componentDidUpdate(_prevProps: CollapsibleSectionProps, prevState: CollapsibleSectionState): void {
|
||||||
if (this.state.isExpanded && this.props.onExpand) {
|
if (!prevState.isExpanded && this.state.isExpanded && this.props.onExpand) {
|
||||||
this.props.onExpand();
|
this.props.onExpand();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack
|
<Stack
|
||||||
className="collapsibleSection"
|
className={"collapsibleSection"}
|
||||||
horizontal
|
horizontal
|
||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
tokens={accordionStackTokens}
|
tokens={accordionStackTokens}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
|
||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
|
@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
|
||||||
* Vertical bar to divide buttons
|
* Vertical bar to divide buttons
|
||||||
*/
|
*/
|
||||||
isDivider?: boolean;
|
isDivider?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aria-label for the button
|
* Aria-label for the button
|
||||||
*/
|
*/
|
||||||
ariaLabel: string;
|
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<CommandButtonComponentProps> {
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
|
|
@ -20,7 +20,10 @@ export interface EditorReactProps {
|
||||||
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
|
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
|
||||||
minimap?: monaco.editor.IEditorOptions["minimap"];
|
minimap?: monaco.editor.IEditorOptions["minimap"];
|
||||||
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
||||||
|
fontSize?: monaco.editor.IEditorOptions["fontSize"];
|
||||||
monacoContainerStyles?: React.CSSProperties;
|
monacoContainerStyles?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
spinnerClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||||
|
@ -46,9 +49,25 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(previous: EditorReactProps) {
|
public componentDidUpdate() {
|
||||||
if (this.props.content !== previous.content) {
|
if (!this.editor) {
|
||||||
this.editor?.setValue(this.props.content);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingContent = this.editor.getModel().getValue();
|
||||||
|
|
||||||
|
if (this.props.content !== existingContent) {
|
||||||
|
if (this.props.isReadOnly) {
|
||||||
|
this.editor.setValue(this.props.content);
|
||||||
|
} else {
|
||||||
|
this.editor.pushUndoStop();
|
||||||
|
this.editor.executeEdits("", [
|
||||||
|
{
|
||||||
|
range: this.editor.getModel().getFullModelRange(),
|
||||||
|
text: this.props.content,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,9 +78,11 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
|
{!this.state.showEditor && (
|
||||||
|
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="jsonEditor"
|
className={this.props.className || "jsonEditor"}
|
||||||
style={this.props.monacoContainerStyles}
|
style={this.props.monacoContainerStyles}
|
||||||
ref={(elt: HTMLElement) => this.setRef(elt)}
|
ref={(elt: HTMLElement) => this.setRef(elt)}
|
||||||
/>
|
/>
|
||||||
|
@ -71,9 +92,14 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
|
|
||||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
const queryEditorModel = this.editor.getModel();
|
|
||||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||||
queryEditorModel.onDidChangeContent(() => {
|
// 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();
|
const queryEditorModel = this.editor.getModel();
|
||||||
this.props.onContentChanged(queryEditorModel.getValue());
|
this.props.onContentChanged(queryEditorModel.getValue());
|
||||||
});
|
});
|
||||||
|
@ -98,7 +124,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
value: this.props.content,
|
value: this.props.content,
|
||||||
readOnly: this.props.isReadOnly,
|
readOnly: this.props.isReadOnly,
|
||||||
ariaLabel: this.props.ariaLabel,
|
ariaLabel: this.props.ariaLabel,
|
||||||
fontSize: 12,
|
fontSize: this.props.fontSize || 12,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
theme: this.props.theme,
|
theme: this.props.theme,
|
||||||
wordWrap: this.props.wordWrap || "off",
|
wordWrap: this.props.wordWrap || "off",
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2ToolTip {
|
.settingsV2ToolTip {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font: 12px @DataExplorerFont;
|
font: 12px @DataExplorerFont;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoPilotSelector span {
|
.autoPilotSelector span {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
font: 14px @DataExplorerFont;
|
font: 14px @DataExplorerFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2TabsContainer {
|
.settingsV2TabsContainer {
|
||||||
|
@ -25,7 +25,14 @@
|
||||||
font-family: @DataExplorerFont;
|
font-family: @DataExplorerFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2IndexingPolicyEditor {
|
.settingsV2Editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settingsV2EditorSpinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import {
|
||||||
|
ComputedPropertiesComponent,
|
||||||
|
ComputedPropertiesComponentProps,
|
||||||
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||||
|
import {
|
||||||
|
ContainerVectorPolicyComponent,
|
||||||
|
ContainerVectorPolicyComponentProps,
|
||||||
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
|
@ -18,6 +28,10 @@ import { userContext } from "../../../UserContext";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import {
|
||||||
|
PartitionKeyComponent,
|
||||||
|
PartitionKeyComponentProps,
|
||||||
|
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
|
@ -103,6 +117,11 @@ export interface SettingsComponentState {
|
||||||
indexesToAdd: AddMongoIndexProps[];
|
indexesToAdd: AddMongoIndexProps[];
|
||||||
indexTransformationProgress: number;
|
indexTransformationProgress: number;
|
||||||
|
|
||||||
|
computedPropertiesContent: DataModels.ComputedProperties;
|
||||||
|
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
||||||
|
shouldDiscardComputedProperties: boolean;
|
||||||
|
isComputedPropertiesDirty: boolean;
|
||||||
|
|
||||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||||
conflictResolutionPolicyPath: string;
|
conflictResolutionPolicyPath: string;
|
||||||
|
@ -127,7 +146,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
private offer: DataModels.Offer;
|
private offer: DataModels.Offer;
|
||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
|
private shouldShowComputedPropertiesEditor: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
|
private isVectorSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
|
@ -139,7 +161,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||||
this.offer = this.collection?.offer();
|
this.offer = this.collection?.offer();
|
||||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||||
|
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
|
||||||
|
@ -191,6 +216,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
indexTransformationProgress: undefined,
|
indexTransformationProgress: undefined,
|
||||||
|
|
||||||
|
computedPropertiesContent: undefined,
|
||||||
|
computedPropertiesContentBaseline: undefined,
|
||||||
|
shouldDiscardComputedProperties: false,
|
||||||
|
isComputedPropertiesDirty: false,
|
||||||
|
|
||||||
conflictResolutionPolicyMode: undefined,
|
conflictResolutionPolicyMode: undefined,
|
||||||
conflictResolutionPolicyModeBaseline: undefined,
|
conflictResolutionPolicyModeBaseline: undefined,
|
||||||
conflictResolutionPolicyPath: undefined,
|
conflictResolutionPolicyPath: undefined,
|
||||||
|
@ -281,6 +311,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
|
this.state.isComputedPropertiesDirty ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -291,6 +322,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
this.state.isSubSettingsDiscardable ||
|
this.state.isSubSettingsDiscardable ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
|
this.state.isComputedPropertiesDirty ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -395,6 +427,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
isMongoIndexingPolicySaveable: false,
|
isMongoIndexingPolicySaveable: false,
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
|
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
|
||||||
|
shouldDiscardComputedProperties: true,
|
||||||
|
isComputedPropertiesDirty: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -514,6 +549,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||||
this.setState({ isMongoIndexingPolicyDiscardable });
|
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 => {
|
private calculateTotalThroughputUsed = (): void => {
|
||||||
this.totalThroughputUsed = 0;
|
this.totalThroughputUsed = 0;
|
||||||
(useDatabases.getState().databases || []).forEach(async (database) => {
|
(useDatabases.getState().databases || []).forEach(async (database) => {
|
||||||
|
@ -636,7 +696,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
||||||
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
||||||
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
||||||
|
@ -645,6 +704,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
const geospatialConfigTypeString: string =
|
const geospatialConfigTypeString: string =
|
||||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||||
|
let computedPropertiesContent = this.collection.computedProperties();
|
||||||
|
if (!computedPropertiesContent || computedPropertiesContent.length === 0) {
|
||||||
|
computedPropertiesContent = [
|
||||||
|
{ name: "name_of_property", query: "query_to_compute_property" },
|
||||||
|
] as DataModels.ComputedProperties;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
|
@ -671,6 +736,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||||
geospatialConfigType: geoSpatialConfigType,
|
geospatialConfigType: geoSpatialConfigType,
|
||||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||||
|
computedPropertiesContent: computedPropertiesContent,
|
||||||
|
computedPropertiesContentBaseline: computedPropertiesContent,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -787,7 +854,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
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;
|
let defaultTtl: number;
|
||||||
switch (this.state.timeToLive) {
|
switch (this.state.timeToLive) {
|
||||||
case TtlType.On:
|
case TtlType.On:
|
||||||
|
@ -825,6 +897,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.isComputedPropertiesDirty) {
|
||||||
|
newCollection.computedProperties = this.state.computedPropertiesContent;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||||
this.collection.databaseId,
|
this.collection.databaseId,
|
||||||
this.collection.id(),
|
this.collection.id(),
|
||||||
|
@ -838,6 +914,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
|
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||||
|
|
||||||
if (wasIndexingPolicyModified) {
|
if (wasIndexingPolicyModified) {
|
||||||
await this.refreshIndexTransformationProgress();
|
await this.refreshIndexTransformationProgress();
|
||||||
|
@ -848,6 +925,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
|
isComputedPropertiesDirty: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1026,6 +1104,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
indexTransformationProgress: this.state.indexTransformationProgress,
|
indexTransformationProgress: this.state.indexTransformationProgress,
|
||||||
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
||||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
|
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
|
||||||
|
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
|
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
|
||||||
|
@ -1042,6 +1121,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
|
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const computedPropertiesComponentProps: ComputedPropertiesComponentProps = {
|
||||||
|
computedPropertiesContent: this.state.computedPropertiesContent,
|
||||||
|
computedPropertiesContentBaseline: this.state.computedPropertiesContentBaseline,
|
||||||
|
logComputedPropertiesSuccessMessage: this.logComputedPropertiesSuccessMessage,
|
||||||
|
onComputedPropertiesContentChange: this.onComputedPropertiesContentChange,
|
||||||
|
onComputedPropertiesDirtyChange: this.onComputedPropertiesDirtyChange,
|
||||||
|
resetShouldDiscardComputedProperties: this.resetShouldDiscardComputedProperties,
|
||||||
|
shouldDiscardComputedProperties: this.state.shouldDiscardComputedProperties,
|
||||||
|
};
|
||||||
|
|
||||||
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
||||||
|
@ -1056,6 +1145,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||||
|
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||||
|
collection: this.collection,
|
||||||
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
||||||
|
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
|
@ -1069,6 +1168,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.isVectorSearchEnabled) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||||
|
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.shouldShowIndexingPolicyEditor) {
|
if (this.shouldShowIndexingPolicyEditor) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
|
@ -1091,6 +1197,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldShowPartitionKeyEditor) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.PartitionKeyTab,
|
||||||
|
content: <PartitionKeyComponent {...partitionKeyComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldShowComputedPropertiesEditor) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.ComputedPropertiesTab,
|
||||||
|
content: <ComputedPropertiesComponent {...computedPropertiesComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
getThroughputApplyLongDelayMessage,
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
getToolTipContainer,
|
getToolTipContainer,
|
||||||
indexingPolicynUnsavedWarningMessage,
|
|
||||||
manualToAutoscaleDisclaimerElement,
|
manualToAutoscaleDisclaimerElement,
|
||||||
mongoIndexTransformationRefreshingMessage,
|
mongoIndexTransformationRefreshingMessage,
|
||||||
mongoIndexingPolicyAADError,
|
mongoIndexingPolicyAADError,
|
||||||
|
@ -39,7 +38,6 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||||
|
|
||||||
{manualToAutoscaleDisclaimerElement}
|
{manualToAutoscaleDisclaimerElement}
|
||||||
{ttlWarning}
|
{ttlWarning}
|
||||||
{indexingPolicynUnsavedWarningMessage}
|
|
||||||
{updateThroughputDelayedApplyWarningMessage}
|
{updateThroughputDelayedApplyWarningMessage}
|
||||||
|
|
||||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
|
|
|
@ -61,6 +61,8 @@ export interface PriceBreakdown {
|
||||||
currencySign: string;
|
currencySign: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type editorType = "indexPolicy" | "computedProperties";
|
||||||
|
|
||||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
|
@ -254,9 +256,10 @@ export const ttlWarning: JSX.Element = (
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
|
||||||
<Text styles={infoAndToolTipTextStyle}>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
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.
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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(<ComputedPropertiesComponent {...baseProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computed properties are reset", () => {
|
||||||
|
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<HTMLDivElement>();
|
||||||
|
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<void> {
|
||||||
|
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 (
|
||||||
|
<Stack {...titleAndInputStackProps}>
|
||||||
|
{isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
|
{unsavedEditorWarningMessage("computedProperties")}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
<Text style={{ marginLeft: "30px", marginBottom: "10px" }}>
|
||||||
|
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
|
||||||
|
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
|
||||||
|
</Link>
|
||||||
|
  about how to define computed properties and how to use them.
|
||||||
|
</Text>
|
||||||
|
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ContainerVectorPolicyComponentProps> = ({
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
|
||||||
|
isReadOnly={true}
|
||||||
|
wordWrap={"on"}
|
||||||
|
ariaLabel={"Container vector policy"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
scrollBeyondLastLine={false}
|
||||||
|
className={"settingsV2Editor"}
|
||||||
|
spinnerClassName={"settingsV2EditorSpinner"}
|
||||||
|
fontSize={14}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,7 +3,7 @@ import * as monaco from "monaco-editor";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
import { loadMonaco } from "../../../LazyMonaco";
|
import { loadMonaco } from "../../../LazyMonaco";
|
||||||
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps {
|
||||||
logIndexingPolicySuccessMessage: () => void;
|
logIndexingPolicySuccessMessage: () => void;
|
||||||
indexTransformationProgress: number;
|
indexTransformationProgress: number;
|
||||||
refreshIndexTransformationProgress: () => Promise<void>;
|
refreshIndexTransformationProgress: () => Promise<void>;
|
||||||
|
isVectorSearchEnabled?: boolean;
|
||||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component<
|
||||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||||
/>
|
/>
|
||||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
{this.props.isVectorSearchEnabled && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
||||||
|
Container vector policies and vector indexes are not modifiable after container creation
|
||||||
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||||
|
)}
|
||||||
|
<div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
addMongoIndexStackProps,
|
addMongoIndexStackProps,
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
customDetailsListStyles,
|
customDetailsListStyles,
|
||||||
indexingPolicynUnsavedWarningMessage,
|
|
||||||
infoAndToolTipTextStyle,
|
infoAndToolTipTextStyle,
|
||||||
mediumWidthStackStyles,
|
mediumWidthStackStyles,
|
||||||
mongoCompoundIndexNotSupportedMessage,
|
mongoCompoundIndexNotSupportedMessage,
|
||||||
|
@ -27,15 +26,16 @@ import {
|
||||||
onRenderRow,
|
onRenderRow,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
|
unsavedEditorWarningMessage,
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
import {
|
import {
|
||||||
AddMongoIndexProps,
|
AddMongoIndexProps,
|
||||||
getMongoIndexType,
|
|
||||||
getMongoIndexTypeText,
|
|
||||||
isIndexTransforming,
|
|
||||||
MongoIndexIdField,
|
MongoIndexIdField,
|
||||||
MongoIndexTypes,
|
MongoIndexTypes,
|
||||||
MongoNotificationType,
|
MongoNotificationType,
|
||||||
|
getMongoIndexType,
|
||||||
|
getMongoIndexTypeText,
|
||||||
|
isIndexTransforming,
|
||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
|
@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||||
if (this.getMongoWarningNotificationMessage()) {
|
if (this.getMongoWarningNotificationMessage()) {
|
||||||
warningMessage = this.getMongoWarningNotificationMessage();
|
warningMessage = this.getMongoWarningNotificationMessage();
|
||||||
} else if (this.isMongoIndexingPolicySaveable()) {
|
} else if (this.isMongoIndexingPolicySaveable()) {
|
||||||
warningMessage = indexingPolicynUnsavedWarningMessage;
|
warningMessage = unsavedEditorWarningMessage("indexPolicy");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
FontWeights,
|
||||||
|
Link,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
|
PrimaryButton,
|
||||||
|
ProgressIndicator,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||||
|
import {
|
||||||
|
CosmosSqlDataTransferDataSourceSink,
|
||||||
|
DataTransferJobGetResults,
|
||||||
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
|
import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { userContext } from "../../../../UserContext";
|
||||||
|
|
||||||
|
export interface PartitionKeyComponentProps {
|
||||||
|
database: ViewModels.Database;
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
|
||||||
|
const { dataTransferJobs } = useDataTransferJobs();
|
||||||
|
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(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",
|
||||||
|
<ChangePartitionKeyPane
|
||||||
|
sourceDatabase={database}
|
||||||
|
sourceCollection={collection}
|
||||||
|
explorer={explorer}
|
||||||
|
onClose={refreshDataTransferOperations}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||||
|
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text>{partitionKeyValue}</Text>
|
||||||
|
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
|
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.
|
||||||
|
<Link
|
||||||
|
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
||||||
|
target="_blank"
|
||||||
|
underline
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
<Text>
|
||||||
|
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.
|
||||||
|
</Text>
|
||||||
|
<PrimaryButton
|
||||||
|
styles={{ root: { width: "fit-content" } }}
|
||||||
|
text="Change"
|
||||||
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||||
|
/>
|
||||||
|
{portalDataTransferJob && (
|
||||||
|
<Stack>
|
||||||
|
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||||
|
<Stack
|
||||||
|
horizontal
|
||||||
|
tokens={{ childrenGap: 20 }}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProgressIndicator
|
||||||
|
label={portalDataTransferJob?.properties?.jobName}
|
||||||
|
description={getProgressDescription()}
|
||||||
|
percentComplete={getPercentageComplete()}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
width: "85%",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></ProgressIndicator>
|
||||||
|
{isCurrentJobInProgress(portalDataTransferJob) && (
|
||||||
|
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||||
};
|
};
|
||||||
|
|
||||||
const costElement = (): JSX.Element => {
|
const costElement = (): JSX.Element => {
|
||||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||||
{newThroughput && newThroughputCostElement()}
|
{newThroughput && newThroughputCostElement()}
|
||||||
|
|
|
@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.012
|
0.0080
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
@ -929,7 +929,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.29
|
0.19
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
@ -941,7 +941,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
8.76
|
5.84
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.012
|
0.0080
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
@ -1366,7 +1366,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.29
|
0.19
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
@ -1378,7 +1378,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
8.76
|
5.84
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ComputedPropertiesComponent renders 1`] = `
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": "10px",
|
||||||
|
"marginLeft": "30px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/computed-properties-preview/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
|
||||||
|
<FontIcon
|
||||||
|
iconName="NavigateExternalInline"
|
||||||
|
/>
|
||||||
|
</StyledLinkBase>
|
||||||
|
about how to define computed properties and how to use them.
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
className="settingsV2Editor"
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
|
@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
|
||||||
refreshIndexTransformationProgress={[Function]}
|
refreshIndexTransformationProgress={[Function]}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="settingsV2IndexingPolicyEditor"
|
className="settingsV2Editor"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
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 TtlOff = "off";
|
||||||
export const TtlOn = "on";
|
export const TtlOn = "on";
|
||||||
export const TtlOnNoDefault = "on-nodefault";
|
export const TtlOnNoDefault = "on-nodefault";
|
||||||
|
@ -45,6 +45,9 @@ export enum SettingsV2TabTypes {
|
||||||
ConflictResolutionTab,
|
ConflictResolutionTab,
|
||||||
SubSettingsTab,
|
SubSettingsTab,
|
||||||
IndexingPolicyTab,
|
IndexingPolicyTab,
|
||||||
|
PartitionKeyTab,
|
||||||
|
ComputedPropertiesTab,
|
||||||
|
ContainerVectorPolicyTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
|
@ -146,6 +149,12 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||||
return "Settings";
|
return "Settings";
|
||||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||||
return "Indexing Policy";
|
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:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
@ -199,3 +208,49 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
|
||||||
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
||||||
// index transformation progress can be 0
|
// index transformation progress can be 0
|
||||||
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
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";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -40,6 +40,12 @@ export const collection = {
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
partitionKeyProperties: ["partitionKey"],
|
partitionKeyProperties: ["partitionKey"],
|
||||||
|
computedProperties: ko.observable<DataModels.ComputedProperties>([
|
||||||
|
{
|
||||||
|
name: "queryName",
|
||||||
|
query: "query",
|
||||||
|
},
|
||||||
|
]),
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,10 +26,10 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"analyticalStorageTtl": [Function],
|
"analyticalStorageTtl": [Function],
|
||||||
"changeFeedPolicy": [Function],
|
"changeFeedPolicy": [Function],
|
||||||
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -103,10 +103,10 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"analyticalStorageTtl": [Function],
|
"analyticalStorageTtl": [Function],
|
||||||
"changeFeedPolicy": [Function],
|
"changeFeedPolicy": [Function],
|
||||||
|
"computedProperties": [Function],
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -196,6 +196,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
"indexingMode": "consistent",
|
"indexingMode": "consistent",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isVectorSearchEnabled={false}
|
||||||
logIndexingPolicySuccessMessage={[Function]}
|
logIndexingPolicySuccessMessage={[Function]}
|
||||||
onIndexingPolicyContentChange={[Function]}
|
onIndexingPolicyContentChange={[Function]}
|
||||||
onIndexingPolicyDirtyChange={[Function]}
|
onIndexingPolicyDirtyChange={[Function]}
|
||||||
|
@ -204,6 +205,131 @@ exports[`SettingsComponent renders 1`] = `
|
||||||
shouldDiscardIndexingPolicy={false}
|
shouldDiscardIndexingPolicy={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
<PivotItem
|
||||||
|
headerText="Partition Keys (preview)"
|
||||||
|
itemKey="PartitionKeyTab"
|
||||||
|
key="PartitionKeyTab"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PartitionKeyComponent
|
||||||
|
collection={
|
||||||
|
Object {
|
||||||
|
"analyticalStorageTtl": [Function],
|
||||||
|
"changeFeedPolicy": [Function],
|
||||||
|
"computedProperties": [Function],
|
||||||
|
"conflictResolutionPolicy": [Function],
|
||||||
|
"container": Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provideFeedbackEmail": [Function],
|
||||||
|
"queriesClient": QueriesClient {
|
||||||
|
"container": [Circular],
|
||||||
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
|
"resourceTree": ResourceTreeAdapter {
|
||||||
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
|
"parameters": [Function],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"databaseId": "test",
|
||||||
|
"defaultTtl": [Function],
|
||||||
|
"geospatialConfig": [Function],
|
||||||
|
"getDatabase": [Function],
|
||||||
|
"id": [Function],
|
||||||
|
"indexingPolicy": [Function],
|
||||||
|
"offer": [Function],
|
||||||
|
"partitionKey": Object {
|
||||||
|
"kind": "hash",
|
||||||
|
"paths": Array [],
|
||||||
|
"version": 2,
|
||||||
|
},
|
||||||
|
"partitionKeyProperties": Array [
|
||||||
|
"partitionKey",
|
||||||
|
],
|
||||||
|
"readSettings": [Function],
|
||||||
|
"uniqueKeyPolicy": Object {},
|
||||||
|
"usageSizeInKB": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
explorer={
|
||||||
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provideFeedbackEmail": [Function],
|
||||||
|
"queriesClient": QueriesClient {
|
||||||
|
"container": [Circular],
|
||||||
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
|
"resourceTree": ResourceTreeAdapter {
|
||||||
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
|
"parameters": [Function],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PivotItem>
|
||||||
|
<PivotItem
|
||||||
|
headerText="Computed Properties"
|
||||||
|
itemKey="ComputedPropertiesTab"
|
||||||
|
key="ComputedPropertiesTab"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ComputedPropertiesComponent
|
||||||
|
computedPropertiesContent={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"name": "queryName",
|
||||||
|
"query": "query",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
computedPropertiesContentBaseline={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"name": "queryName",
|
||||||
|
"query": "query",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
logComputedPropertiesSuccessMessage={[Function]}
|
||||||
|
onComputedPropertiesContentChange={[Function]}
|
||||||
|
onComputedPropertiesDirtyChange={[Function]}
|
||||||
|
resetShouldDiscardComputedProperties={[Function]}
|
||||||
|
shouldDiscardComputedProperties={false}
|
||||||
|
/>
|
||||||
|
</PivotItem>
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -99,18 +99,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"color": "windowtext",
|
|
||||||
"fontSize": 14,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
|
||||||
</Text>
|
|
||||||
<Text
|
<Text
|
||||||
id="updateThroughputDelayedApplyWarningMessage"
|
id="updateThroughputDelayedApplyWarningMessage"
|
||||||
styles={
|
styles={
|
||||||
|
|
|
@ -14,7 +14,13 @@
|
||||||
.throughputInputSpacing > :not(:last-child) {
|
.throughputInputSpacing > :not(:last-child) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
.capacitycalculator-link:focus{
|
|
||||||
|
.capacitycalculator-link:focus {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copyQuery:focus::after,
|
||||||
|
.deleteQuery:focus::after {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
|
@ -247,7 +247,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||||
name="More"
|
name="More"
|
||||||
title="More"
|
title="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
ariaLabel={menuItemLabel}
|
ariaLabel={`${menuItemLabel} options`}
|
||||||
menuIconProps={{
|
menuIconProps={{
|
||||||
iconName: menuItemLabel,
|
iconName: menuItemLabel,
|
||||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
||||||
|
|
|
@ -172,7 +172,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
>
|
>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="More"
|
ariaLabel="More options"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
menuIconProps={
|
menuIconProps={
|
||||||
Object {
|
Object {
|
||||||
|
@ -397,7 +397,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
>
|
>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="More"
|
ariaLabel="More options"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
menuIconProps={
|
menuIconProps={
|
||||||
Object {
|
Object {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
|
@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
|
||||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||||
|
@ -265,61 +263,43 @@ export default class Explorer {
|
||||||
// TODO: return result
|
// TODO: return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRandomInt(max: number) {
|
|
||||||
return Math.floor(Math.random() * max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openNPSSurveyDialog(): void {
|
public openNPSSurveyDialog(): void {
|
||||||
if (!Platform.Portal) {
|
if (!Platform.Portal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NINETY_DAYS_IN_MS = 7776000000;
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
const THREE_DAYS_IN_MS = 259200000;
|
const SEVEN_DAYS_IN_MS = 604800000;
|
||||||
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
|
|
||||||
userContext.databaseAccount?.systemData?.createdAt || "",
|
|
||||||
NINETY_DAYS_IN_MS,
|
|
||||||
);
|
|
||||||
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
|
|
||||||
|
|
||||||
if (lastSubmitted !== null) {
|
|
||||||
let lastSubmittedDate: number = parseInt(lastSubmitted);
|
|
||||||
if (isNaN(lastSubmittedDate)) {
|
|
||||||
lastSubmittedDate = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nowMs: number = Date.now();
|
|
||||||
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
|
|
||||||
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
||||||
if (userContext.isTryCosmosDBSubscription) {
|
if (userContext.isTryCosmosDBSubscription) {
|
||||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
||||||
this.sendNPSMessage();
|
Logger.logInfo(
|
||||||
|
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
|
||||||
|
"Explorer/openNPSSurveyDialog",
|
||||||
|
);
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// An existing account is older than 3 days but less than 90 days old. For existing account show to 100% of users in Data Explorer.
|
// Show survey when an existing account is older than 7 days
|
||||||
if (
|
if (
|
||||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) &&
|
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
||||||
isAccountNewerThanNinetyDays
|
|
||||||
) {
|
) {
|
||||||
this.sendNPSMessage();
|
Logger.logInfo(
|
||||||
} else {
|
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
||||||
// An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer.
|
"Explorer/openNPSSurveyDialog",
|
||||||
if (this.getRandomInt(100) < 33) {
|
);
|
||||||
this.sendNPSMessage();
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendNPSMessage() {
|
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||||
localStorage.setItem("lastSubmitted", Date.now().toString());
|
Logger.logInfo(
|
||||||
|
`CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`,
|
||||||
|
"Explorer/openCESCVAFeedbackBlade",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refreshDatabaseForResourceToken(): Promise<void> {
|
public async refreshDatabaseForResourceToken(): Promise<void> {
|
||||||
|
@ -528,104 +508,6 @@ export default class Explorer {
|
||||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
.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<boolean> {
|
|
||||||
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(
|
private getDeltaDatabases(
|
||||||
updatedDatabaseList: DataModels.Database[],
|
updatedDatabaseList: DataModels.Database[],
|
||||||
databases: ViewModels.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<void> {
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<p>{Notebook.newNotebookModalContent1}</p>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{Notebook.newNotebookModalContent2}
|
|
||||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
|
||||||
{Notebook.learnMore}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||||
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||||
|
@ -1148,10 +944,6 @@ export default class Explorer {
|
||||||
let title: string;
|
let title: string;
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case ViewModels.TerminalKind.Default:
|
|
||||||
title = "Terminal";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ViewModels.TerminalKind.Mongo:
|
case ViewModels.TerminalKind.Mongo:
|
||||||
title = "Mongo Shell";
|
title = "Mongo Shell";
|
||||||
break;
|
break;
|
||||||
|
@ -1305,36 +1097,6 @@ export default class Explorer {
|
||||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
|
||||||
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component<
|
||||||
onActivated={this.setIsDeleteConfirm.bind(this, true)}
|
onActivated={this.setIsDeleteConfirm.bind(this, true)}
|
||||||
aria-label="Delete this vertex"
|
aria-label="Delete this vertex"
|
||||||
>
|
>
|
||||||
<img src={DeleteIcon} alt="Delete" />
|
<img src={DeleteIcon} alt="Delete" role="button" />
|
||||||
</AccessibleElement>
|
</AccessibleElement>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component<
|
||||||
aria-label="Edit properties"
|
aria-label="Edit properties"
|
||||||
onActivated={expandClickHandler}
|
onActivated={expandClickHandler}
|
||||||
>
|
>
|
||||||
<img src={EditIcon} alt="Edit" />
|
<img src={EditIcon} alt="Edit" role="button" />
|
||||||
</AccessibleElement>
|
</AccessibleElement>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||||
className="rightPaneTrashIcon rightPaneBtns"
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
|
aria-label={`Delete ${data.key}`}
|
||||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
|
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
|
||||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
|
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
|
||||||
removeNewVertexPropertyKeyPress(event, index)
|
removeNewVertexPropertyKeyPress(event, index)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
|
<img
|
||||||
|
aria-label="hidden"
|
||||||
|
className="refreshcol rightPaneTrashIconImg"
|
||||||
|
src={DeleteIcon}
|
||||||
|
alt="Remove property"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
|
@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons =
|
const buttons =
|
||||||
|
@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
|
|
|
@ -2,10 +2,8 @@ import * as ko from "knockout";
|
||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import NotebookManager from "../../Notebook/NotebookManager";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
|
@ -72,181 +70,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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", () => {
|
describe("Open Cassandra shell button", () => {
|
||||||
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
const openCassandraShellBtnLabel = "Open Cassandra shell";
|
||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
@ -305,42 +128,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
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", () => {
|
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", () => {
|
describe("Resource token", () => {
|
||||||
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
const mockCollection = { id: ko.observable("test") } as CollectionBase;
|
||||||
useSelectedNode.getState().setSelectedNode(mockCollection);
|
useSelectedNode.getState().setSelectedNode(mockCollection);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||||
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
||||||
|
@ -6,13 +8,10 @@ import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||||
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
||||||
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
||||||
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
|
||||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||||
|
import HomeIcon from "../../../../images/Home_16.svg";
|
||||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.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 OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||||
|
@ -20,7 +19,6 @@ import { AuthType } from "../../../AuthType";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
|
@ -31,7 +29,6 @@ import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
|
||||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
|
@ -57,7 +54,11 @@ export function createStaticCommandBarButtons(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
|
const homeBtn = createHomeButton();
|
||||||
|
buttons.push(homeBtn);
|
||||||
|
|
||||||
const newCollectionBtn = createNewCollectionGroup(container);
|
const newCollectionBtn = createNewCollectionGroup(container);
|
||||||
|
newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below.
|
||||||
buttons.push(newCollectionBtn);
|
buttons.push(newCollectionBtn);
|
||||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
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()) {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
|
||||||
|
@ -135,7 +85,7 @@ export function createStaticCommandBarButtons(
|
||||||
buttons.push(newSqlQueryBtn);
|
buttons.push(newSqlQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
||||||
const openQueryBtn = createOpenQueryButton(container);
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
|
@ -146,6 +96,7 @@ export function createStaticCommandBarButtons(
|
||||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddStoredProcedureIcon,
|
iconSrc: AddStoredProcedureIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_SPROC,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
|
@ -196,18 +147,22 @@ export function createContextCommandBarButtons(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] =
|
||||||
{
|
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
||||||
iconSrc: SettingsIcon,
|
? []
|
||||||
iconAlt: "Settings",
|
: [
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
{
|
||||||
commandButtonLabel: undefined,
|
iconSrc: SettingsIcon,
|
||||||
ariaLabel: "Settings",
|
iconAlt: "Settings",
|
||||||
tooltipText: "Settings",
|
onCommandClick: () =>
|
||||||
hasPopup: true,
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
disabled: false,
|
commandButtonLabel: undefined,
|
||||||
},
|
ariaLabel: "Settings",
|
||||||
];
|
tooltipText: "Settings",
|
||||||
|
hasPopup: true,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const showOpenFullScreen =
|
const showOpenFullScreen =
|
||||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||||
|
@ -231,12 +186,12 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||||
buttons.push(fullScreenButton);
|
buttons.push(fullScreenButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Emulator) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
const label = "Feedback";
|
const label = "Feedback";
|
||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.provideFeedbackEmail(),
|
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: 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 {
|
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -313,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||||
return {
|
return {
|
||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_DATABASE,
|
||||||
onCommandClick: async () => {
|
onCommandClick: async () => {
|
||||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
if (throughputCap && throughputCap !== -1) {
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
@ -333,6 +301,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||||
id: "newQueryBtn",
|
id: "newQueryBtn",
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||||
|
@ -348,6 +317,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||||
id: "newQueryBtn",
|
id: "newQueryBtn",
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_QUERY,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||||
|
@ -373,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddStoredProcedureIcon,
|
iconSrc: AddStoredProcedureIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_SPROC,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
|
@ -392,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||||
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
|
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddUdfIcon,
|
iconSrc: AddUdfIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_UDF,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||||
|
@ -411,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||||
const newTriggerBtn: CommandButtonComponentProps = {
|
const newTriggerBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: AddTriggerIcon,
|
iconSrc: AddTriggerIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_TRIGGER,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
|
||||||
|
@ -428,45 +401,12 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||||
return buttons;
|
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 {
|
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "Open Query";
|
const label = "Open Query";
|
||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: () =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
|
@ -481,6 +421,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||||
return {
|
return {
|
||||||
iconSrc: OpenQueryFromDiskIcon,
|
iconSrc: OpenQueryFromDiskIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: 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(
|
function createOpenTerminalButtonByKind(
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
terminalKind: ViewModels.TerminalKind,
|
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,
|
|
||||||
<GitHubReposPanel
|
|
||||||
explorer={container}
|
|
||||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
|
||||||
junoClientProp={junoClient}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
commandButtonLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
ariaLabel: label,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
container: Explorer,
|
container: Explorer,
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { KeyboardHandlerMap } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||||
|
@ -37,7 +38,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
return StyleConstants.GrayScale;
|
return StyleConstants.GrayScale;
|
||||||
}
|
}
|
||||||
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
|
return configContext.platform == Platform.Fabric ? StyleConstants.FabricToolbarIconColor : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return btns
|
return btns
|
||||||
|
@ -96,7 +97,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||||
},
|
},
|
||||||
width: 16,
|
width: 16,
|
||||||
},
|
},
|
||||||
label: { fontSize: StyleConstants.mediumFontSize },
|
label: {
|
||||||
|
fontSize:
|
||||||
|
configContext.platform == Platform.Fabric
|
||||||
|
? StyleConstants.DefaultFontSize
|
||||||
|
: StyleConstants.mediumFontSize,
|
||||||
|
},
|
||||||
rootHovered: { backgroundColor: hoverColor },
|
rootHovered: { backgroundColor: hoverColor },
|
||||||
rootPressed: { backgroundColor: hoverColor },
|
rootPressed: { backgroundColor: hoverColor },
|
||||||
splitButtonMenuButtonExpanded: {
|
splitButtonMenuButtonExpanded: {
|
||||||
|
@ -133,7 +139,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
// 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
|
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||||
selectors: {
|
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-link:hover": { backgroundColor: hoverColor },
|
||||||
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
||||||
},
|
},
|
||||||
|
@ -223,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
||||||
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -162,6 +162,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||||
role="button"
|
role="button"
|
||||||
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
|
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
style={{ border: "1px solid black", borderRadius: "2px" }}
|
||||||
>
|
>
|
||||||
<img src={ClearIcon} alt="clear notifications image" />
|
<img src={ClearIcon} alt="clear notifications image" />
|
||||||
Clear Notifications
|
Clear Notifications
|
||||||
|
|
|
@ -146,6 +146,12 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
role="button"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1px solid black",
|
||||||
|
"borderRadius": "2px",
|
||||||
|
}
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -311,6 +317,12 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
role="button"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1px solid black",
|
||||||
|
"borderRadius": "2px",
|
||||||
|
}
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
@ -40,97 +41,112 @@ function openCollectionTab(
|
||||||
databases: ViewModels.Database[],
|
databases: ViewModels.Database[],
|
||||||
initialDatabaseIndex = 0,
|
initialDatabaseIndex = 0,
|
||||||
) {
|
) {
|
||||||
for (let i = initialDatabaseIndex; i < databases.length; i++) {
|
//if databases are not yet loaded, wait until loaded
|
||||||
const database: ViewModels.Database = databases[i];
|
if (!databases || databases.length === 0) {
|
||||||
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
|
const databaseActionHandler = (databases: ViewModels.Database[]) => {
|
||||||
continue;
|
databasesUnsubscription();
|
||||||
}
|
openCollectionTab(action, databases, 0);
|
||||||
|
return;
|
||||||
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 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));
|
//expand database first if not expanded to load the collections
|
||||||
if (database.collections && database.collections() && database.collections().length) {
|
if (!database.isDatabaseExpanded?.()) {
|
||||||
collectionActionHandler(database.collections());
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||||
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||||
|
@ -81,6 +82,26 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||||
excludedPaths: [],
|
excludedPaths: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: [
|
||||||
|
{
|
||||||
|
path: '/"_etag"/?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vectorIndexes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
|
vectorEmbeddings: [],
|
||||||
|
};
|
||||||
|
|
||||||
export interface AddCollectionPanelState {
|
export interface AddCollectionPanelState {
|
||||||
createNewDatabase: boolean;
|
createNewDatabase: boolean;
|
||||||
newDatabaseId: string;
|
newDatabaseId: string;
|
||||||
|
@ -101,6 +122,8 @@ export interface AddCollectionPanelState {
|
||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
isThroughputCapExceeded: boolean;
|
isThroughputCapExceeded: boolean;
|
||||||
teachingBubbleStep: number;
|
teachingBubbleStep: number;
|
||||||
|
vectorIndexingPolicy: string;
|
||||||
|
vectorEmbeddingPolicy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
|
@ -136,6 +159,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
isThroughputCapExceeded: false,
|
isThroughputCapExceeded: false,
|
||||||
teachingBubbleStep: 0,
|
teachingBubbleStep: 0,
|
||||||
|
vectorIndexingPolicy: JSON.stringify(DefaultDatabaseVectorIndex, null, 2),
|
||||||
|
vectorEmbeddingPolicy: JSON.stringify(DefaultVectorEmbeddingPolicy, null, 2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,11 +170,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
||||||
|
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
||||||
|
this.scrollToSection("panelContainer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
|
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
|
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)} id="panelContainer">
|
||||||
{this.state.errorMessage && (
|
{this.state.errorMessage && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={this.state.errorMessage}
|
message={this.state.errorMessage}
|
||||||
|
@ -382,6 +413,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
)}
|
)}
|
||||||
{!this.state.createNewDatabase && (
|
{!this.state.createNewDatabase && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
ariaLabel="Choose an existing database"
|
||||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||||
style={{ width: 300, fontSize: 12 }}
|
style={{ width: 300, fontSize: 12 }}
|
||||||
placeholder="Choose an existing database"
|
placeholder="Choose an existing database"
|
||||||
|
@ -863,17 +895,76 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
{this.shouldShowVectorSearchParameters() && (
|
||||||
|
<Stack>
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
title="Indexing Policy"
|
||||||
|
isExpandedByDefault={false}
|
||||||
|
onExpand={() => {
|
||||||
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
|
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={this.state.vectorIndexingPolicy}
|
||||||
|
isReadOnly={false}
|
||||||
|
wordWrap={"on"}
|
||||||
|
ariaLabel={"Editing indexing policy"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
scrollBeyondLastLine={false}
|
||||||
|
spinnerClassName="panelSectionSpinner"
|
||||||
|
monacoContainerStyles={{
|
||||||
|
minHeight: 200,
|
||||||
|
}}
|
||||||
|
onContentChanged={(newIndexingPolicy: string) => this.setVectorIndexingPolicy(newIndexingPolicy)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
title="Container Vector Policy"
|
||||||
|
isExpandedByDefault={false}
|
||||||
|
onExpand={() => {
|
||||||
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
|
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={this.state.vectorEmbeddingPolicy}
|
||||||
|
isReadOnly={false}
|
||||||
|
wordWrap={"on"}
|
||||||
|
ariaLabel={"Editing container vector policy"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
scrollBeyondLastLine={false}
|
||||||
|
spinnerClassName="panelSectionSpinner"
|
||||||
|
monacoContainerStyles={{
|
||||||
|
minHeight: 200,
|
||||||
|
}}
|
||||||
|
onContentChanged={(newVectorEmbeddingPolicy: string) =>
|
||||||
|
this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
{userContext.apiType !== "Tables" && (
|
{userContext.apiType !== "Tables" && (
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
this.scrollToAdvancedSection();
|
this.scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
|
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
|
@ -924,10 +1015,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> To ensure compatibility with
|
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the
|
||||||
older SDKs, the created container will use a legacy partitioning scheme that supports partition
|
created container will use a legacy partitioning scheme that supports partition key values of size
|
||||||
key values of size only up to 101 bytes. If this is enabled, you will not be able to use
|
only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
|
||||||
hierarchical partition keys.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1070,6 +1160,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: string): void {
|
||||||
|
this.setState({
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setVectorIndexingPolicy(vectorIndexingPolicy: string): void {
|
||||||
|
this.setState({
|
||||||
|
vectorIndexingPolicy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private isSelectedDatabaseSharedThroughput(): boolean {
|
private isSelectedDatabaseSharedThroughput(): boolean {
|
||||||
if (!this.state.selectedDatabaseId) {
|
if (!this.state.selectedDatabaseId) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1209,6 +1311,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldShowVectorSearchParameters() {
|
||||||
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
|
}
|
||||||
|
|
||||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||||
if (this.state.uniqueKeys?.length === 0) {
|
if (this.state.uniqueKeys?.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -1265,6 +1371,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
|
try {
|
||||||
|
JSON.parse(this.state.vectorIndexingPolicy) as DataModels.IndexingPolicy;
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ errorMessage: "Invalid JSON format for indexingPolicy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(this.state.vectorEmbeddingPolicy) as DataModels.VectorEmbeddingPolicy;
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ errorMessage: "Invalid JSON format for vectorEmbeddingPolicy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1287,8 +1409,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
return Constants.AnalyticalStorageTtl.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollToAdvancedSection(): void {
|
private scrollToSection(id: string): void {
|
||||||
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
|
document.getElementById(id)?.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSampleDBName(): string {
|
private getSampleDBName(): string {
|
||||||
|
@ -1340,10 +1462,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
let indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||||
? AllPropertiesIndexed
|
? AllPropertiesIndexed
|
||||||
: SharedDatabaseDefault;
|
: SharedDatabaseDefault;
|
||||||
|
|
||||||
|
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||||
|
|
||||||
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
|
indexingPolicy = JSON.parse(this.state.vectorIndexingPolicy);
|
||||||
|
vectorEmbeddingPolicy = JSON.parse(this.state.vectorEmbeddingPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
database: {
|
database: {
|
||||||
id: databaseId,
|
id: databaseId,
|
||||||
|
@ -1402,6 +1531,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
partitionKey,
|
partitionKey,
|
||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({ isExecuting: true });
|
this.setState({ isExecuting: true });
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||||
|
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import { SubscriptionType } from "../../../Contracts/SubscriptionType";
|
import { SubscriptionType } from "../../../Contracts/SubscriptionType";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
@ -14,6 +13,7 @@ import { userContext } from "../../../UserContext";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "../../../Utils/PricingUtils";
|
import { getUpsellMessage } from "../../../Utils/PricingUtils";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
|
@ -63,9 +63,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||||
},
|
},
|
||||||
subscriptionType: SubscriptionType[subscriptionType],
|
subscriptionType: SubscriptionType[subscriptionType],
|
||||||
subscriptionQuotaId: userContext.quotaId,
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
|
||||||
flight: userContext.addCollectionFlight,
|
|
||||||
},
|
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,7 +72,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||||
subscriptionQuotaId: userContext.quotaId,
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
throughput,
|
throughput,
|
||||||
flight: userContext.addCollectionFlight,
|
|
||||||
},
|
},
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,7 +59,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
throughput: newKeySpaceThroughput || tableThroughput,
|
throughput: newKeySpaceThroughput || tableThroughput,
|
||||||
flight: userContext.addCollectionFlight,
|
|
||||||
},
|
},
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
|
@ -203,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
styles={getTextFieldStyles()}
|
styles={getTextFieldStyles()}
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Type a new keyspace id"
|
placeholder="Type a new keyspace id"
|
||||||
size={40}
|
size={40}
|
||||||
value={newKeyspaceId}
|
value={newKeyspaceId}
|
||||||
|
@ -293,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||||
required={true}
|
required={true}
|
||||||
ariaLabel="addCollection-table Id Create table"
|
ariaLabel="addCollection-table Id Create table"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Enter table Id"
|
placeholder="Enter table Id"
|
||||||
size={20}
|
size={20}
|
||||||
value={tableId}
|
value={tableId}
|
||||||
|
|
|
@ -0,0 +1,398 @@
|
||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
DirectionalHint,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
MessageBar,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TooltipHost,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import * as Constants from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
|
import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import * as ViewModels from "Contracts/ViewModels";
|
||||||
|
import {
|
||||||
|
getPartitionKeyName,
|
||||||
|
getPartitionKeyPlaceHolder,
|
||||||
|
getPartitionKeySubtext,
|
||||||
|
getPartitionKeyTooltipText,
|
||||||
|
} from "Explorer/Controls/Settings/SettingsUtils";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface ChangePartitionKeyPaneProps {
|
||||||
|
sourceDatabase: ViewModels.Database;
|
||||||
|
sourceCollection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
onClose: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||||
|
sourceDatabase,
|
||||||
|
sourceCollection,
|
||||||
|
explorer,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [targetCollectionId, setTargetCollectionId] = React.useState<string>();
|
||||||
|
const [createNewContainer, setCreateNewContainer] = React.useState<boolean>(true);
|
||||||
|
const [formError, setFormError] = React.useState<string>();
|
||||||
|
const [isExecuting, setIsExecuting] = React.useState<boolean>(false);
|
||||||
|
const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]);
|
||||||
|
const [partitionKey, setPartitionKey] = React.useState<string>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
|
||||||
|
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
|
||||||
|
<Text variant="small">
|
||||||
|
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.
|
||||||
|
<Link
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
|
||||||
|
target="_blank"
|
||||||
|
underline
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Database id
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
<Dropdown
|
||||||
|
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||||
|
style={{ width: 300, fontSize: 12 }}
|
||||||
|
options={[]}
|
||||||
|
placeholder={sourceDatabase.id()}
|
||||||
|
responsiveMode={999}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack className="panelGroupSpacing" horizontal verticalAlign="center">
|
||||||
|
<div role="radiogroup">
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={createNewContainer}
|
||||||
|
aria-label="Create new container"
|
||||||
|
aria-checked={createNewContainer}
|
||||||
|
name="containerType"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
id="containerCreateNew"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={() => setCreateNewContainer(true)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">New container</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={!createNewContainer}
|
||||||
|
aria-label="Use existing container"
|
||||||
|
aria-checked={!createNewContainer}
|
||||||
|
name="containerType"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={() => setCreateNewContainer(false)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Existing container</span>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
{createNewContainer ? (
|
||||||
|
<Stack>
|
||||||
|
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{`${getCollectionName()} id`}
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
role="button"
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
<input
|
||||||
|
name="collectionId"
|
||||||
|
id="collectionId"
|
||||||
|
type="text"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||||||
|
value={targetCollectionId}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{getPartitionKeyName(userContext.apiType)}
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={getPartitionKeyTooltipText(userContext.apiType)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={getPartitionKeyTooltipText(userContext.apiType)}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Text variant="small" aria-label="pkDescription">
|
||||||
|
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addCollection-partitionKeyValue"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
placeholder={getPartitionKeyPlaceHolder(userContext.apiType)}
|
||||||
|
aria-label={getPartitionKeyName(userContext.apiType)}
|
||||||
|
pattern={".*"}
|
||||||
|
title={""}
|
||||||
|
value={partitionKey}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!partitionKey && !event.target.value.startsWith("/")) {
|
||||||
|
setPartitionKey("/" + event.target.value);
|
||||||
|
} else {
|
||||||
|
setPartitionKey(event.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "20px",
|
||||||
|
border: "solid",
|
||||||
|
borderWidth: "0px 0px 1px 1px",
|
||||||
|
marginRight: "5px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addCollection-partitionKeyValue"
|
||||||
|
key={`addCollection-partitionKeyValue_${index}`}
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
tabIndex={index > 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<HTMLInputElement>) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27 }}
|
||||||
|
onClick={() => {
|
||||||
|
const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j);
|
||||||
|
setSubPartitionKeys(keys);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<DefaultButton
|
||||||
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
|
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||||
|
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||||
|
>
|
||||||
|
Add hierarchical partition key
|
||||||
|
</DefaultButton>
|
||||||
|
{subPartitionKeys.length > 0 && (
|
||||||
|
<Text variant="small">
|
||||||
|
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={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.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
{`${getCollectionName()}`}
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
role="button"
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||||
|
style={{ width: 300, fontSize: 12 }}
|
||||||
|
placeholder="Choose an existing container"
|
||||||
|
options={getCollectionOptions()}
|
||||||
|
onChange={(event: React.FormEvent<HTMLDivElement>, collection: IDropdownOption) => {
|
||||||
|
setTargetCollectionId(collection.key as string);
|
||||||
|
setFormError("");
|
||||||
|
}}
|
||||||
|
defaultSelectedKey={targetCollectionId}
|
||||||
|
responsiveMode={999}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</RightPaneForm>
|
||||||
|
);
|
||||||
|
};
|
|
@ -2140,6 +2140,11 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</DeleteCollectionConfirmationPane>
|
</DeleteCollectionConfirmationPane>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -7073,6 +7073,11 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</ExecuteSprocParamsPane>
|
</ExecuteSprocParamsPane>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
|
|
@ -48,6 +48,13 @@
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
padding: 0 @LargeSpace 0 @SmallSpace;
|
padding: 0 @LargeSpace 0 @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panelSectionSpinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||||
onDismiss={this.onDissmiss}
|
onDismiss={this.onDissmiss}
|
||||||
isLightDismiss
|
isLightDismiss
|
||||||
type={PanelType.custom}
|
type={PanelType.custom}
|
||||||
closeButtonAriaLabel="Close"
|
closeButtonAriaLabel={`Close ${this.props.headerText}`}
|
||||||
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
|
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
|
||||||
headerClassName="panelHeader"
|
headerClassName="panelHeader"
|
||||||
onRenderNavigationContent={this.props.onRenderNavigationContent}
|
onRenderNavigationContent={this.props.onRenderNavigationContent}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { CSSProperties, FunctionComponent, ReactNode } from "react";
|
||||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||||
|
import { labelToLoadingItemName } from "Explorer/Tables/Constants";
|
||||||
|
|
||||||
export interface RightPaneFormProps {
|
export interface RightPaneFormProps {
|
||||||
formError: string;
|
formError: string;
|
||||||
|
@ -27,6 +28,10 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
||||||
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
|
const screenReaderStatusElement = document.getElementById("screenReaderStatus");
|
||||||
|
if (screenReaderStatusElement) {
|
||||||
|
screenReaderStatusElement.innerHTML = labelToLoadingItemName[submitButtonText] || "Loading";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -42,6 +47,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
<span role="status" className="screenReaderOnly" id="screenReaderStatus"></span>
|
||||||
{isExecuting && <PanelLoadingScreen />}
|
{isExecuting && <PanelLoadingScreen />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1782,5 +1782,10 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -11,7 +11,13 @@ import {
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { configContext } from "ConfigContext";
|
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 * as StringUtility from "Shared/StringUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
|
@ -35,6 +41,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
? Constants.Queries.UnlimitedPageOption
|
? Constants.Queries.UnlimitedPageOption
|
||||||
: Constants.Queries.CustomPageOption,
|
: Constants.Queries.CustomPageOption,
|
||||||
);
|
);
|
||||||
|
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||||
|
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||||
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
||||||
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||||
);
|
);
|
||||||
|
@ -103,6 +111,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||||
);
|
);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval);
|
LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval);
|
||||||
|
@ -120,6 +129,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ruThresholdEnabled) {
|
||||||
|
LocalStorageUtility.setEntryNumber(StorageKey.RUThreshold, ruThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
if (queryTimeoutEnabled) {
|
if (queryTimeoutEnabled) {
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
||||||
LocalStorageUtility.setEntryBoolean(
|
LocalStorageUtility.setEntryBoolean(
|
||||||
|
@ -195,6 +208,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
|
setRUThresholdEnabled(checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnRUThresholdSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
||||||
|
const ruThreshold = Number(newValue);
|
||||||
|
if (!isNaN(ruThreshold)) {
|
||||||
|
setRUThreshold(ruThreshold);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
setQueryTimeoutEnabled(checked);
|
setQueryTimeoutEnabled(checked);
|
||||||
};
|
};
|
||||||
|
@ -234,7 +258,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
|
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
|
||||||
setCopilotSampleDBEnabled(checked);
|
setCopilotSampleDBEnabled(checked);
|
||||||
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
|
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
|
||||||
setRefreshExplorer(!refreshExplorer);
|
setRefreshExplorer(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const choiceButtonStyles = {
|
const choiceButtonStyles = {
|
||||||
|
@ -259,7 +283,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryTimeoutToggleStyles: IToggleStyles = {
|
const toggleStyles: IToggleStyles = {
|
||||||
label: {
|
label: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
|
@ -272,7 +296,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
text: {},
|
text: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = {
|
const spinButtonStyles: ISpinButtonStyles = {
|
||||||
label: {
|
label: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
|
@ -338,48 +362,83 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<div className="settingsSection">
|
<>
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSection">
|
||||||
<div>
|
<div className="settingsSectionPart">
|
||||||
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
<div>
|
||||||
Query Timeout
|
<legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
|
||||||
</legend>
|
RU Threshold
|
||||||
<InfoTooltip>
|
</legend>
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
<InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
|
||||||
unless automatic cancellation has been enabled
|
</div>
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
styles={queryTimeoutToggleStyles}
|
|
||||||
label="Enable query timeout"
|
|
||||||
onChange={handleOnQueryTimeoutToggleChange}
|
|
||||||
defaultChecked={queryTimeoutEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{queryTimeoutEnabled && (
|
|
||||||
<div>
|
<div>
|
||||||
<SpinButton
|
|
||||||
label="Query timeout (ms)"
|
|
||||||
labelPosition={Position.top}
|
|
||||||
defaultValue={(queryTimeout || 5000).toString()}
|
|
||||||
min={100}
|
|
||||||
step={1000}
|
|
||||||
onChange={handleOnQueryTimeoutSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
|
||||||
styles={queryTimeoutSpinButtonStyles}
|
|
||||||
/>
|
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Automatically cancel query after timeout"
|
styles={toggleStyles}
|
||||||
styles={queryTimeoutToggleStyles}
|
label="Enable RU threshold"
|
||||||
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
onChange={handleOnRUThresholdToggleChange}
|
||||||
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
defaultChecked={ruThresholdEnabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{ruThresholdEnabled && (
|
||||||
|
<div>
|
||||||
|
<SpinButton
|
||||||
|
label="RU Threshold (RU)"
|
||||||
|
labelPosition={Position.top}
|
||||||
|
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||||
|
min={1}
|
||||||
|
step={1000}
|
||||||
|
onChange={handleOnRUThresholdSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
|
styles={spinButtonStyles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<div>
|
||||||
|
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
Query Timeout
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
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
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
styles={toggleStyles}
|
||||||
|
label="Enable query timeout"
|
||||||
|
onChange={handleOnQueryTimeoutToggleChange}
|
||||||
|
defaultChecked={queryTimeoutEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{queryTimeoutEnabled && (
|
||||||
|
<div>
|
||||||
|
<SpinButton
|
||||||
|
label="Query timeout (ms)"
|
||||||
|
labelPosition={Position.top}
|
||||||
|
defaultValue={(queryTimeout || 5000).toString()}
|
||||||
|
min={100}
|
||||||
|
step={1000}
|
||||||
|
onChange={handleOnQueryTimeoutSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
|
styles={spinButtonStyles}
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Automatically cancel query after timeout"
|
||||||
|
styles={toggleStyles}
|
||||||
|
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
||||||
|
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
|
@ -404,7 +463,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||||
styles={queryTimeoutSpinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
@ -426,7 +485,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||||
styles={queryTimeoutSpinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
@ -448,7 +507,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||||
styles={queryTimeoutSpinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -571,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
Enable sample database
|
Enable sample database
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
This is a sample database and collection with synthetic product data you can use to explore using
|
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.
|
created by, and maintained by Microsoft at no cost to you.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -581,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
}}
|
}}
|
||||||
className="padding"
|
className="padding"
|
||||||
ariaLabel="Enable sample db for Copilot"
|
ariaLabel="Enable sample db for Query Advisor"
|
||||||
checked={copilotSampleDBEnabled}
|
checked={copilotSampleDBEnabled}
|
||||||
onChange={handleSampleDatabaseChange}
|
onChange={handleSampleDatabaseChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -97,6 +97,74 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<legend
|
||||||
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="ruThresholdLabel"
|
||||||
|
>
|
||||||
|
RU Threshold
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
If a query exceeds a configured RU threshold, the query will be aborted.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StyledToggleBase
|
||||||
|
defaultChecked={true}
|
||||||
|
label="Enable RU threshold"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"container": Object {},
|
||||||
|
"label": Object {
|
||||||
|
"display": "block",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"pill": Object {},
|
||||||
|
"root": Object {},
|
||||||
|
"text": Object {},
|
||||||
|
"thumb": Object {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StyledSpinButton
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
|
defaultValue="5000"
|
||||||
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
|
label="RU Threshold (RU)"
|
||||||
|
labelPosition={0}
|
||||||
|
min={1}
|
||||||
|
onChange={[Function]}
|
||||||
|
step={1000}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"arrowButtonsContainer": Object {},
|
||||||
|
"icon": Object {},
|
||||||
|
"input": Object {},
|
||||||
|
"label": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"labelWrapper": Object {},
|
||||||
|
"root": Object {
|
||||||
|
"paddingBottom": 10,
|
||||||
|
},
|
||||||
|
"spinButtonWrapper": Object {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
@ -2450,6 +2449,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</StringInputPane>
|
</StringInputPane>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||||
|
@ -97,9 +98,19 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||||
/* Add new entity attribute */
|
/* Add new entity attribute */
|
||||||
const onSubmit = async (): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
const { property, type } = entities[i];
|
const { property, type, value } = entities[i];
|
||||||
if (property === "" || property === undefined) {
|
if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) {
|
||||||
setFormError(`Property name cannot be empty. Please enter a property name`);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,12 +118,14 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFormError("");
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
|
||||||
try {
|
try {
|
||||||
|
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||||
tableEntityListViewModel.redrawTableThrottled();
|
tableEntityListViewModel.redrawTableThrottled();
|
||||||
|
@ -127,6 +140,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const containsAnyWhiteSpace = (entityValue: string) => {
|
||||||
|
if (/\s/.test(entityValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||||
let newHeaders: string[] = [];
|
let newHeaders: string[] = [];
|
||||||
const keys = Object.keys(newEntity);
|
const keys = Object.keys(newEntity);
|
||||||
|
@ -182,9 +202,14 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||||
const entityChange = (value: string | Date, indexOfInput: number, key: string): void => {
|
const entityChange = (value: string | Date, indexOfInput: number, key: string): void => {
|
||||||
const cloneEntities: EntityRowType[] = [...entities];
|
const cloneEntities: EntityRowType[] = [...entities];
|
||||||
if (key === "property") {
|
if (key === "property") {
|
||||||
cloneEntities[indexOfInput].property = value.toString();
|
cloneEntities[indexOfInput].property = value.toString().trim();
|
||||||
} else if (key === "time") {
|
} else if (key === "time") {
|
||||||
cloneEntities[indexOfInput].entityTimeValue = value.toString();
|
cloneEntities[indexOfInput].entityTimeValue = value.toString();
|
||||||
|
} else if (
|
||||||
|
cloneEntities[indexOfInput].property === "PartitionKey" ||
|
||||||
|
cloneEntities[indexOfInput].property === "RowKey"
|
||||||
|
) {
|
||||||
|
cloneEntities[indexOfInput].value = value.toString().trim();
|
||||||
} else {
|
} else {
|
||||||
cloneEntities[indexOfInput].value = value.toString();
|
cloneEntities[indexOfInput].value = value.toString();
|
||||||
}
|
}
|
||||||
|
@ -236,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||||
<TextField
|
<TextField
|
||||||
multiline
|
multiline
|
||||||
rows={5}
|
rows={5}
|
||||||
|
ariaLabel={entityAttributeProperty}
|
||||||
value={entityAttributeValue}
|
value={entityAttributeValue}
|
||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
entityChange(newInput, selectedRow, "value");
|
entityChange(newInput, selectedRow, "value");
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||||
|
@ -190,7 +191,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||||
|
|
||||||
const onSubmit = async (): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
const { property, type } = entities[i];
|
const { property, type, value } = entities[i];
|
||||||
if (property === "" || property === undefined) {
|
if (property === "" || property === undefined) {
|
||||||
setFormError(`Property name cannot be empty. Please enter a property name`);
|
setFormError(`Property name cannot be empty. Please enter a property name`);
|
||||||
return;
|
return;
|
||||||
|
@ -200,6 +201,17 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||||
return;
|
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);
|
setIsExecuting(true);
|
||||||
|
@ -359,7 +371,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||||
selectedKey={entity.type}
|
selectedKey={entity.type}
|
||||||
entityPropertyPlaceHolder={detailedHelp}
|
entityPropertyPlaceHolder={detailedHelp}
|
||||||
entityValuePlaceholder={entity.entityValuePlaceholder}
|
entityValuePlaceholder={entity.entityValuePlaceholder}
|
||||||
entityValue={entity.value?.toString()}
|
entityValue={entity.value.toString()}
|
||||||
isEntityTypeDate={entity.isEntityTypeDate}
|
isEntityTypeDate={entity.isEntityTypeDate}
|
||||||
entityTimeValue={entity.entityTimeValue}
|
entityTimeValue={entity.entityTimeValue}
|
||||||
isEntityValueDisable={entity.isEntityValueDisable}
|
isEntityValueDisable={entity.isEntityValueDisable}
|
||||||
|
|
|
@ -3019,6 +3019,11 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</TableQuerySelectPanel>
|
</TableQuerySelectPanel>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2130,6 +2130,11 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</AddTableEntityPanel>
|
</AddTableEntityPanel>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2136,6 +2136,11 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</EditTableEntityPanel>
|
</EditTableEntityPanel>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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<NotebookContentItem>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({ 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<FileList>();
|
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(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<HTMLInputElement>): void => {
|
|
||||||
setFiles(event.target.files);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
|
|
||||||
const readFileAsText = (inputFile: File): Promise<string> => {
|
|
||||||
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 (
|
|
||||||
<RightPaneForm {...props}>
|
|
||||||
<div className="paneMainContent">
|
|
||||||
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
|
|
||||||
</div>
|
|
||||||
</RightPaneForm>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -3,6 +3,7 @@
|
||||||
exports[`AddCollectionPanel should render Default properly 1`] = `
|
exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||||
<form
|
<form
|
||||||
className="panelFormWrapper"
|
className="panelFormWrapper"
|
||||||
|
id="panelContainer"
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -433,7 +434,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
id="collapsibleSectionContent"
|
id="collapsibleAdvancedSectionContent"
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
|
@ -466,7 +467,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||||
<Icon
|
<Icon
|
||||||
className="removeIcon"
|
className="removeIcon"
|
||||||
iconName="InfoSolid"
|
iconName="InfoSolid"
|
||||||
tabIndex={0}
|
|
||||||
/>
|
/>
|
||||||
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.
|
||||||
|
|
||||||
|
|
|
@ -2810,6 +2810,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</PanelFooterComponent>
|
</PanelFooterComponent>
|
||||||
</form>
|
</form>
|
||||||
|
<span
|
||||||
|
className="screenReaderOnly"
|
||||||
|
id="screenReaderStatus"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
</DeleteDatabaseConfirmationPanel>
|
</DeleteDatabaseConfirmationPanel>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = `
|
exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = `
|
||||||
<StyledPanelBase
|
<StyledPanelBase
|
||||||
closeButtonAriaLabel="Close"
|
closeButtonAriaLabel="Close test"
|
||||||
customWidth="440px"
|
customWidth="440px"
|
||||||
headerClassName="panelHeader"
|
headerClassName="panelHeader"
|
||||||
headerText="test"
|
headerText="test"
|
||||||
|
@ -42,7 +42,7 @@ exports[`PaneContainerComponent test should render nothing if content is undefin
|
||||||
|
|
||||||
exports[`PaneContainerComponent test should render with panel content and header 1`] = `
|
exports[`PaneContainerComponent test should render with panel content and header 1`] = `
|
||||||
<StyledPanelBase
|
<StyledPanelBase
|
||||||
closeButtonAriaLabel="Close"
|
closeButtonAriaLabel="Close test"
|
||||||
customWidth="440px"
|
customWidth="440px"
|
||||||
headerClassName="panelHeader"
|
headerClassName="panelHeader"
|
||||||
headerText="test"
|
headerText="test"
|
||||||
|
|
|
@ -49,7 +49,7 @@ export const QueryCopilotFeedbackModal = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={showFeedbackModal}>
|
<Modal isOpen={showFeedbackModal} styles={{ main: { borderRadius: 8, maxWidth: 600 } }}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Stack style={{ padding: 24 }}>
|
<Stack style={{ padding: 24 }}>
|
||||||
<Stack horizontal horizontalAlign="space-between">
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
|
@ -68,9 +68,14 @@ export const QueryCopilotFeedbackModal = ({
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
styles={{ root: { marginBottom: 14 } }}
|
styles={{
|
||||||
|
root: { marginBottom: 14 },
|
||||||
|
fieldGroup: { backgroundColor: "#F3F2F1", borderRadius: 4, borderColor: "#D1D1D1" },
|
||||||
|
}}
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
defaultValue={generatedQuery}
|
defaultValue={generatedQuery}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
|
|
|
@ -50,7 +50,9 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontalAlign="center">
|
<Stack horizontalAlign="center">
|
||||||
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
||||||
<Text className="title bold">Welcome to Microsoft Copilot for Azure in Cosmos DB</Text>
|
<Text className="title bold" as={"h1"}>
|
||||||
|
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
|
||||||
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -67,9 +75,16 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
@ -125,6 +140,14 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||||
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={false}
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -189,9 +212,16 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
@ -247,6 +277,14 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||||
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={false}
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -311,9 +349,16 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
@ -369,6 +414,14 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||||
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={false}
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -433,9 +486,16 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
@ -491,6 +551,14 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||||
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={false}
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -555,9 +623,16 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
@ -613,6 +688,14 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||||
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -677,9 +760,16 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
@ -750,6 +840,14 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||||
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
|
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={false}
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"borderRadius": 8,
|
||||||
|
"maxWidth": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -814,9 +912,16 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
defaultValue="test query"
|
defaultValue="test query"
|
||||||
label="Query generated"
|
label="Query generated"
|
||||||
|
multiline={true}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
|
rows={3}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"backgroundColor": "#F3F2F1",
|
||||||
|
"borderColor": "#D1D1D1",
|
||||||
|
"borderRadius": 4,
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"marginBottom": 14,
|
"marginBottom": 14,
|
||||||
},
|
},
|
||||||
|
|
|
@ -67,9 +67,10 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
as="h1"
|
||||||
className="title bold"
|
className="title bold"
|
||||||
>
|
>
|
||||||
Welcome to Microsoft Copilot for Azure in Cosmos DB
|
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem
|
<StackItem
|
||||||
|
|
|
@ -30,6 +30,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
|
showPromptTeachingBubble: true,
|
||||||
showDeletePopup: false,
|
showDeletePopup: false,
|
||||||
showFeedbackBar: false,
|
showFeedbackBar: false,
|
||||||
showCopyPopup: false,
|
showCopyPopup: false,
|
||||||
|
@ -65,6 +66,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||||
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
|
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
|
||||||
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
|
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
|
||||||
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
|
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
|
||||||
|
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }),
|
||||||
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
|
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
|
||||||
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
|
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
|
||||||
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
|
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
|
||||||
|
@ -103,6 +105,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
|
showPromptTeachingBubble: true,
|
||||||
showDeletePopup: false,
|
showDeletePopup: false,
|
||||||
showFeedbackBar: false,
|
showFeedbackBar: false,
|
||||||
showCopyPopup: false,
|
showCopyPopup: false,
|
||||||
|
|
|
@ -11,18 +11,16 @@ import {
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType,
|
||||||
|
ProgressIndicator,
|
||||||
Separator,
|
Separator,
|
||||||
Spinner,
|
|
||||||
Stack,
|
Stack,
|
||||||
TeachingBubble,
|
TeachingBubble,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
|
||||||
import { HttpStatusCodes } from "Common/Constants";
|
import { HttpStatusCodes } from "Common/Constants";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
|
||||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||||
import {
|
import {
|
||||||
|
@ -38,7 +36,6 @@ import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import HintIcon from "../../../images/Hint.svg";
|
import HintIcon from "../../../images/Hint.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
|
||||||
import RecentIcon from "../../../images/Recent.svg";
|
import RecentIcon from "../../../images/Recent.svg";
|
||||||
import errorIcon from "../../../images/close-black.svg";
|
import errorIcon from "../../../images/close-black.svg";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
@ -71,7 +68,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
databaseId,
|
databaseId,
|
||||||
containerId,
|
containerId,
|
||||||
}: QueryCopilotPromptProps): JSX.Element => {
|
}: QueryCopilotPromptProps): JSX.Element => {
|
||||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const {
|
const {
|
||||||
openFeedbackModal,
|
openFeedbackModal,
|
||||||
|
@ -94,6 +91,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
setIsSamplePromptsOpen,
|
setIsSamplePromptsOpen,
|
||||||
showSamplePrompts,
|
showSamplePrompts,
|
||||||
setShowSamplePrompts,
|
setShowSamplePrompts,
|
||||||
|
showPromptTeachingBubble,
|
||||||
|
setShowPromptTeachingBubble,
|
||||||
showDeletePopup,
|
showDeletePopup,
|
||||||
setShowDeletePopup,
|
setShowDeletePopup,
|
||||||
showFeedbackBar,
|
showFeedbackBar,
|
||||||
|
@ -215,12 +214,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (generateSQLQueryResponse?.sql !== "N/A") {
|
if (generateSQLQueryResponse?.sql !== "N/A") {
|
||||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
const queryExplanation = `-- **Explanation of query:** ${
|
||||||
if (generateSQLQueryResponse.explanation) {
|
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
|
||||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
}\r\n`;
|
||||||
}
|
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
|
||||||
query += generateSQLQueryResponse.sql;
|
const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
|
||||||
setQuery(query);
|
setQuery(`${lastQuery}${currentGeneratedQuery}`);
|
||||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
setShowFeedbackBar(true);
|
setShowFeedbackBar(true);
|
||||||
|
@ -271,19 +270,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showTeachingBubble = (): void => {
|
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
|
||||||
if (!inputEdited.current) {
|
setCopilotTeachingBubbleVisible(visible);
|
||||||
setTimeout(() => {
|
setShowPromptTeachingBubble(visible);
|
||||||
if (!inputEdited.current && !isWelcomModalVisible()) {
|
|
||||||
toggleCopilotTeachingBubbleVisible();
|
|
||||||
inputEdited.current = true;
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWelcomModalVisible = (): boolean => {
|
|
||||||
return localStorage.getItem("hideWelcomeModal") !== "true";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFeedback = () => {
|
const clearFeedback = () => {
|
||||||
|
@ -314,19 +303,394 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
showTeachingBubble();
|
|
||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className="copilot-prompt-pane"
|
className="copilot-prompt-pane"
|
||||||
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
|
styles={{ root: { backgroundColor: "#FAFAFA", padding: "8px" } }}
|
||||||
id="copilot-textfield-label"
|
id="copilot-textfield-label"
|
||||||
>
|
>
|
||||||
<Stack horizontal>
|
<Stack
|
||||||
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
|
horizontal
|
||||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
styles={{
|
||||||
|
root: {
|
||||||
|
width: "100%",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: "#D1D1D1",
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0px 4px 8px 0px #00000024",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack style={{ width: "100%" }}>
|
||||||
|
<Stack horizontal verticalAlign="center" style={{ padding: "8px 8px 0px 8px" }}>
|
||||||
|
<TextField
|
||||||
|
id="naturalLanguageInput"
|
||||||
|
value={userPrompt}
|
||||||
|
onChange={handleUserPromptChange}
|
||||||
|
onClick={() => {
|
||||||
|
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 (
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Send" }}
|
||||||
|
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||||
|
style={{ background: "none" }}
|
||||||
|
onClick={() => startGenerateQueryProcess()}
|
||||||
|
aria-label="Send"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
|
||||||
|
<TeachingBubble
|
||||||
|
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||||
|
target="#naturalLanguageInput"
|
||||||
|
hasCloseButton={true}
|
||||||
|
closeButtonAriaLabel="Close"
|
||||||
|
onDismiss={() => 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{" "}
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
setShowSamplePrompts(true);
|
||||||
|
toggleCopilotTeachingBubbleVisible(false);
|
||||||
|
}}
|
||||||
|
style={{ color: "white", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
sample prompts
|
||||||
|
</Link>{" "}
|
||||||
|
or write your own query
|
||||||
|
</TeachingBubble>
|
||||||
|
)}
|
||||||
|
{showSamplePrompts && (
|
||||||
|
<Callout
|
||||||
|
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
||||||
|
target="#naturalLanguageInput"
|
||||||
|
isBeakVisible={false}
|
||||||
|
onDismiss={() => setShowSamplePrompts(false)}
|
||||||
|
directionalHintFixed={true}
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
alignTargetEdge={true}
|
||||||
|
gapSpace={4}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
{filteredHistories?.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#0078D4",
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Recent
|
||||||
|
</Text>
|
||||||
|
{filteredHistories.map((history, i) => (
|
||||||
|
<DefaultButton
|
||||||
|
key={i}
|
||||||
|
onClick={() => {
|
||||||
|
setUserPrompt(history);
|
||||||
|
setShowSamplePrompts(false);
|
||||||
|
inputEdited.current = true;
|
||||||
|
}}
|
||||||
|
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
|
||||||
|
styles={promptStyles}
|
||||||
|
>
|
||||||
|
{history}
|
||||||
|
</DefaultButton>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{filteredSuggestedPrompts?.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#0078D4",
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Suggested Prompts
|
||||||
|
</Text>
|
||||||
|
{filteredSuggestedPrompts.map((prompt) => (
|
||||||
|
<DefaultButton
|
||||||
|
key={prompt.id}
|
||||||
|
onClick={() => {
|
||||||
|
setUserPrompt(prompt.text);
|
||||||
|
setShowSamplePrompts(false);
|
||||||
|
inputEdited.current = true;
|
||||||
|
}}
|
||||||
|
onRenderIcon={() => <Image src={HintIcon} />}
|
||||||
|
styles={promptStyles}
|
||||||
|
>
|
||||||
|
{prompt.text}
|
||||||
|
</DefaultButton>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||||
|
<Stack>
|
||||||
|
<Separator
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
selectors: { "::before": { background: "#E1DFDD" } },
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Learn about{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
|
||||||
|
writing effective prompts
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{!isGeneratingQuery && (
|
||||||
|
<Stack style={{ padding: 8 }}>
|
||||||
|
{!showFeedbackBar && (
|
||||||
|
<Text style={{ fontSize: 12 }}>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
|
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}>
|
||||||
|
Read preview terms
|
||||||
|
</Link>
|
||||||
|
{showErrorMessageBar && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.error}>
|
||||||
|
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
{showInvalidQueryMessageBar && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.info}
|
||||||
|
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
||||||
|
>
|
||||||
|
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
|
||||||
|
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
||||||
|
this article.
|
||||||
|
</Link>{" "}
|
||||||
|
Our content guidelines can be found
|
||||||
|
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
|
||||||
|
here.
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{showFeedbackBar && (
|
||||||
|
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
||||||
|
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||||
|
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||||
|
<Callout
|
||||||
|
role="status"
|
||||||
|
style={{ padding: "6px 12px" }}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
beakCurtain: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
calloutMain: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
target="#likeBtn"
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowCallout(false);
|
||||||
|
SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
generatedQuery: generatedQuery,
|
||||||
|
likeQuery: likeQuery,
|
||||||
|
description: "",
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
},
|
||||||
|
explorer,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
|
mode: isSampleCopilotActive ? "Sample" : "User",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
directionalHint={DirectionalHint.topCenter}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
Thank you. Need to give{" "}
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
setShowCallout(false);
|
||||||
|
openFeedbackModal(generatedQuery, true, userPrompt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
more feedback?
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
id="likeBtn"
|
||||||
|
style={{ marginLeft: 10 }}
|
||||||
|
aria-label="Like"
|
||||||
|
role="toggle"
|
||||||
|
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||||
|
onClick={() => {
|
||||||
|
setShowCallout(!likeQuery);
|
||||||
|
setLikeQuery(!likeQuery);
|
||||||
|
if (likeQuery === true) {
|
||||||
|
document.getElementById("likeStatus").innerHTML = "Unpressed";
|
||||||
|
}
|
||||||
|
if (likeQuery === false) {
|
||||||
|
document.getElementById("likeStatus").innerHTML = "Liked";
|
||||||
|
}
|
||||||
|
if (dislikeQuery) {
|
||||||
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
style={{ margin: "0 4px" }}
|
||||||
|
role="toggle"
|
||||||
|
aria-label="Dislike"
|
||||||
|
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||||
|
onClick={() => {
|
||||||
|
let toggleStatusValue = "Unpressed";
|
||||||
|
if (!dislikeQuery) {
|
||||||
|
openFeedbackModal(generatedQuery, false, userPrompt);
|
||||||
|
setLikeQuery(false);
|
||||||
|
toggleStatusValue = "Disliked";
|
||||||
|
}
|
||||||
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
setShowCallout(false);
|
||||||
|
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
|
||||||
|
<Separator
|
||||||
|
vertical
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
"::after": {
|
||||||
|
backgroundColor: "#767676",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<CommandBarButton
|
||||||
|
className="copyQuery"
|
||||||
|
onClick={copyGeneratedCode}
|
||||||
|
iconProps={{ iconName: "Copy" }}
|
||||||
|
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy code
|
||||||
|
</CommandBarButton>
|
||||||
|
<CommandBarButton
|
||||||
|
className="deleteQuery"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeletePopup(true);
|
||||||
|
}}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear editor
|
||||||
|
</CommandBarButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{isGeneratingQuery && (
|
||||||
|
<ProgressIndicator
|
||||||
|
label="Thinking..."
|
||||||
|
ariaLabel={getAriaLabel()}
|
||||||
|
barHeight={4}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
fontSize: 12,
|
||||||
|
width: "100%",
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
itemName: {
|
||||||
|
padding: "0px 8px",
|
||||||
|
},
|
||||||
|
itemProgress: {
|
||||||
|
borderBottomLeftRadius: 8,
|
||||||
|
borderBottomRightRadius: 8,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
progressBar: {
|
||||||
|
backgroundImage:
|
||||||
|
"linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(24, 90, 189) 35%, rgb(71, 207, 250) 70%, rgb(180, 124, 248) 92%, rgba(0, 0, 0, 0))",
|
||||||
|
animationDuration: "5s",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ imageProps: { src: errorIcon } }}
|
iconProps={{ imageProps: { src: errorIcon } }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -334,284 +698,10 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
clearFeedback();
|
clearFeedback();
|
||||||
resetMessageStates();
|
resetMessageStates();
|
||||||
}}
|
}}
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
marginLeft: "auto !important",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
ariaLabel="Close"
|
ariaLabel="Close"
|
||||||
|
title="Close copilot"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<TextField
|
|
||||||
id="naturalLanguageInput"
|
|
||||||
value={userPrompt}
|
|
||||||
onChange={handleUserPromptChange}
|
|
||||||
onClick={() => {
|
|
||||||
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 && (
|
|
||||||
<TeachingBubble
|
|
||||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
|
||||||
target="#naturalLanguageInput"
|
|
||||||
hasCloseButton={true}
|
|
||||||
closeButtonAriaLabel="Close"
|
|
||||||
onDismiss={toggleCopilotTeachingBubbleVisible}
|
|
||||||
hasSmallHeadline={true}
|
|
||||||
headline="Write a prompt"
|
|
||||||
>
|
|
||||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
|
||||||
<Link
|
|
||||||
onClick={() => {
|
|
||||||
setShowSamplePrompts(true);
|
|
||||||
toggleCopilotTeachingBubbleVisible();
|
|
||||||
}}
|
|
||||||
style={{ color: "white", fontWeight: 600 }}
|
|
||||||
>
|
|
||||||
sample prompts
|
|
||||||
</Link>{" "}
|
|
||||||
or write your own query
|
|
||||||
</TeachingBubble>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Send" }}
|
|
||||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
|
||||||
style={{ marginLeft: 8 }}
|
|
||||||
onClick={() => startGenerateQueryProcess()}
|
|
||||||
aria-label="Send"
|
|
||||||
/>
|
|
||||||
<div role="alert" aria-label={getAriaLabel()}>
|
|
||||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
|
||||||
</div>
|
|
||||||
{showSamplePrompts && (
|
|
||||||
<Callout
|
|
||||||
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
|
||||||
target="#naturalLanguageInput"
|
|
||||||
isBeakVisible={false}
|
|
||||||
onDismiss={() => setShowSamplePrompts(false)}
|
|
||||||
directionalHintFixed={true}
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
alignTargetEdge={true}
|
|
||||||
gapSpace={4}
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
{filteredHistories?.length > 0 && (
|
|
||||||
<Stack>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#0078D4",
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Recent
|
|
||||||
</Text>
|
|
||||||
{filteredHistories.map((history, i) => (
|
|
||||||
<DefaultButton
|
|
||||||
key={i}
|
|
||||||
onClick={() => {
|
|
||||||
setUserPrompt(history);
|
|
||||||
setShowSamplePrompts(false);
|
|
||||||
inputEdited.current = true;
|
|
||||||
}}
|
|
||||||
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
|
|
||||||
styles={promptStyles}
|
|
||||||
>
|
|
||||||
{history}
|
|
||||||
</DefaultButton>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{filteredSuggestedPrompts?.length > 0 && (
|
|
||||||
<Stack>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#0078D4",
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Suggested Prompts
|
|
||||||
</Text>
|
|
||||||
{filteredSuggestedPrompts.map((prompt) => (
|
|
||||||
<DefaultButton
|
|
||||||
key={prompt.id}
|
|
||||||
onClick={() => {
|
|
||||||
setUserPrompt(prompt.text);
|
|
||||||
setShowSamplePrompts(false);
|
|
||||||
inputEdited.current = true;
|
|
||||||
}}
|
|
||||||
onRenderIcon={() => <Image src={HintIcon} />}
|
|
||||||
styles={promptStyles}
|
|
||||||
>
|
|
||||||
{prompt.text}
|
|
||||||
</DefaultButton>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
|
||||||
<Stack>
|
|
||||||
<Separator
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
selectors: { "::before": { background: "#E1DFDD" } },
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Learn about{" "}
|
|
||||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
|
|
||||||
writing effective prompts
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Callout>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack style={{ margin: "8px 0" }}>
|
|
||||||
<Text style={{ fontSize: 12 }}>
|
|
||||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
|
||||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072c9" }}>
|
|
||||||
Read preview terms
|
|
||||||
</Link>
|
|
||||||
{showErrorMessageBar && (
|
|
||||||
<MessageBar messageBarType={MessageBarType.error}>
|
|
||||||
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{showInvalidQueryMessageBar && (
|
|
||||||
<MessageBar
|
|
||||||
messageBarType={MessageBarType.info}
|
|
||||||
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
|
||||||
>
|
|
||||||
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
|
|
||||||
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
|
||||||
this article.
|
|
||||||
</Link>{" "}
|
|
||||||
Our content guidelines can be found
|
|
||||||
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
|
|
||||||
here.
|
|
||||||
</Link>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{showFeedbackBar && (
|
|
||||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
|
||||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
|
||||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
|
||||||
<Callout
|
|
||||||
style={{ padding: 8 }}
|
|
||||||
target="#likeBtn"
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowCallout(false);
|
|
||||||
SubmitFeedback({
|
|
||||||
params: {
|
|
||||||
generatedQuery: generatedQuery,
|
|
||||||
likeQuery: likeQuery,
|
|
||||||
description: "",
|
|
||||||
userPrompt: userPrompt,
|
|
||||||
},
|
|
||||||
explorer,
|
|
||||||
databaseId,
|
|
||||||
containerId,
|
|
||||||
mode: isSampleCopilotActive ? "Sample" : "User",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
directionalHint={DirectionalHint.topCenter}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
Thank you. Need to give{" "}
|
|
||||||
<Link
|
|
||||||
onClick={() => {
|
|
||||||
setShowCallout(false);
|
|
||||||
openFeedbackModal(generatedQuery, true, userPrompt);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
more feedback?
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Callout>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
id="likeBtn"
|
|
||||||
style={{ marginLeft: 20 }}
|
|
||||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
|
||||||
aria-label="Like"
|
|
||||||
onClick={() => {
|
|
||||||
setShowCallout(!likeQuery);
|
|
||||||
setLikeQuery(!likeQuery);
|
|
||||||
if (dislikeQuery) {
|
|
||||||
setDislikeQuery(!dislikeQuery);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
style={{ margin: "0 10px" }}
|
|
||||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
|
||||||
onClick={() => {
|
|
||||||
if (!dislikeQuery) {
|
|
||||||
openFeedbackModal(generatedQuery, false, userPrompt);
|
|
||||||
setLikeQuery(false);
|
|
||||||
}
|
|
||||||
setDislikeQuery(!dislikeQuery);
|
|
||||||
setShowCallout(false);
|
|
||||||
}}
|
|
||||||
aria-label="Dislike"
|
|
||||||
/>
|
|
||||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
|
||||||
<CommandBarButton
|
|
||||||
onClick={copyGeneratedCode}
|
|
||||||
iconProps={{ iconName: "Copy" }}
|
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
|
||||||
>
|
|
||||||
Copy query
|
|
||||||
</CommandBarButton>
|
|
||||||
<CommandBarButton
|
|
||||||
onClick={() => {
|
|
||||||
setShowDeletePopup(true);
|
|
||||||
}}
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
|
||||||
>
|
|
||||||
Delete query
|
|
||||||
</CommandBarButton>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
<WelcomeModal visible={isWelcomModalVisible()} />
|
|
||||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||||
{query !== "" && query.trim().length !== 0 && (
|
{query !== "" && query.trim().length !== 0 && (
|
||||||
<DeletePopup
|
<DeletePopup
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotCl
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
@ -37,7 +37,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||||
const executeQueryBtn = {
|
const executeQueryBtn = {
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: executeQueryBtnLabel,
|
iconAlt: executeQueryBtnLabel,
|
||||||
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot),
|
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot as Partial<QueryCopilotState>),
|
||||||
commandButtonLabel: executeQueryBtnLabel,
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
ariaLabel: executeQueryBtnLabel,
|
ariaLabel: executeQueryBtnLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||||
|
|
||||||
const toggleCopilotButton = {
|
const toggleCopilotButton = {
|
||||||
iconSrc: QueryCommandIcon,
|
iconSrc: QueryCommandIcon,
|
||||||
iconAlt: "Copilot",
|
iconAlt: "Query Advisor",
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
toggleCopilot(true);
|
toggleCopilot(true);
|
||||||
},
|
},
|
||||||
commandButtonLabel: "Copilot",
|
commandButtonLabel: "Query Advisor",
|
||||||
ariaLabel: "Copilot",
|
ariaLabel: "Query Advisor",
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: copilotActive,
|
disabled: copilotActive,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
|
BackendApi,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
HttpStatusCodes,
|
HttpStatusCodes,
|
||||||
|
@ -30,6 +31,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
@ -80,7 +82,11 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||||
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 authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const QueryCopilotResults: React.FC = (): JSX.Element => {
|
export const QueryCopilotResults: React.FC = (): JSX.Element => {
|
||||||
|
@ -12,7 +12,11 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => {
|
||||||
queryResults={useQueryCopilot.getState().queryResults}
|
queryResults={useQueryCopilot.getState().queryResults}
|
||||||
isExecuting={useQueryCopilot.getState().isExecuting}
|
isExecuting={useQueryCopilot.getState().isExecuting}
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot)
|
QueryDocumentsPerPage(
|
||||||
|
firstItemIndex,
|
||||||
|
useQueryCopilot.getState().queryIterator,
|
||||||
|
useQueryCopilot as Partial<QueryCopilotState>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
|
|
@ -25,7 +25,6 @@ import * as React from "react";
|
||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
import ContainersIcon from "../../../images/Containers.svg";
|
import ContainersIcon from "../../../images/Containers.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
import LinkIcon from "../../../images/Link_blue.svg";
|
||||||
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
|
||||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||||
|
@ -148,12 +147,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
{useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && (
|
{useQueryCopilot.getState().copilotEnabled && (
|
||||||
<SplashScreenButton
|
<SplashScreenButton
|
||||||
imgSrc={CopilotIcon}
|
imgSrc={CopilotIcon}
|
||||||
title={"Query faster with Copilot"}
|
title={"Query faster with Query Advisor"}
|
||||||
description={
|
description={
|
||||||
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const copilotVersion = userContext.features.copilotVersion;
|
const copilotVersion = userContext.features.copilotVersion;
|
||||||
|
@ -410,14 +409,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
heroes.push(launchQuickstartBtn);
|
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());
|
heroes.push(this.getShellCard());
|
||||||
|
@ -689,11 +680,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
title: "Learn the Fundamentals",
|
title: "Learn the Fundamentals",
|
||||||
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
|
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) {
|
switch (userContext.apiType) {
|
||||||
case "SQL":
|
case "SQL":
|
||||||
case "Postgres":
|
case "Postgres":
|
||||||
items = [
|
apiItems = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/msl-sdk-connect",
|
link: "https://aka.ms/msl-sdk-connect",
|
||||||
title: "Get Started using an SDK",
|
title: "Get Started using an SDK",
|
||||||
|
@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Mongo":
|
case "Mongo":
|
||||||
items = [
|
apiItems = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/mongonodejs",
|
link: "https://aka.ms/mongonodejs",
|
||||||
title: "Build an app with Node.js",
|
title: "Build an app with Node.js",
|
||||||
|
@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Cassandra":
|
case "Cassandra":
|
||||||
items = [
|
apiItems = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/cassandracontainer",
|
link: "https://aka.ms/cassandracontainer",
|
||||||
title: "Create a Container",
|
title: "Create a Container",
|
||||||
|
@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Gremlin":
|
case "Gremlin":
|
||||||
items = [
|
apiItems = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/graphquickstart",
|
link: "https://aka.ms/graphquickstart",
|
||||||
title: "Get Started ",
|
title: "Get Started ",
|
||||||
|
@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Tables":
|
case "Tables":
|
||||||
items = [
|
apiItems = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/tabledotnet",
|
link: "https://aka.ms/tabledotnet",
|
||||||
title: "Build a .NET App",
|
title: "Build a .NET App",
|
||||||
|
@ -770,6 +770,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const items = [...commonItems, ...apiItems];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
|
|
|
@ -225,3 +225,8 @@ export const InputType = {
|
||||||
DateTime: "datetime-local",
|
DateTime: "datetime-local",
|
||||||
Number: "number",
|
Number: "number",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const labelToLoadingItemName: Record<string, string> = {
|
||||||
|
"Add Row": "Adding row to table",
|
||||||
|
"Add Entity": "Adding entity to table",
|
||||||
|
};
|
||||||
|
|
|
@ -42,6 +42,11 @@ function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewM
|
||||||
|
|
||||||
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
|
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
|
||||||
$(window).resize(updateTableScrollableRegionMetrics);
|
$(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) {
|
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
|
||||||
|
@ -210,6 +215,39 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi
|
||||||
});
|
});
|
||||||
//selected = bindingContext.$data.selected();
|
//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) {
|
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
|
||||||
// do nothing for now
|
// do nothing for now
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
|
@ -171,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
deferred.resolve(entity);
|
deferred.resolve(entity);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
|
@ -261,6 +263,57 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
query: string,
|
query: string,
|
||||||
shouldNotify?: boolean,
|
shouldNotify?: boolean,
|
||||||
paginationToken?: string,
|
paginationToken?: string,
|
||||||
|
): Promise<Entities.IListTableEntitiesResult> {
|
||||||
|
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<Entities.IListTableEntitiesResult> {
|
): Promise<Entities.IListTableEntitiesResult> {
|
||||||
const clearMessage =
|
const clearMessage =
|
||||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||||
|
@ -294,7 +347,11 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
shouldNotify &&
|
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;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage?.();
|
clearMessage?.();
|
||||||
|
@ -350,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
errorText,
|
||||||
"CreateKeyspaceCassandra",
|
"CreateKeyspaceCassandra",
|
||||||
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
`Error while creating a keyspace with query ${createKeyspaceQuery}`,
|
||||||
);
|
);
|
||||||
deferred.reject(error);
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
|
@ -388,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(
|
||||||
|
errorText,
|
||||||
|
"CreateTableCassandra",
|
||||||
|
`Error while creating a table with query ${createTableQuery}`,
|
||||||
|
);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(clearInProgressMessage);
|
.finally(clearInProgressMessage);
|
||||||
|
@ -402,6 +465,51 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||||
|
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<CassandraTableKeys>();
|
||||||
|
|
||||||
|
$.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<CassandraTableKeys> {
|
||||||
if (!!collection.cassandraKeys) {
|
if (!!collection.cassandraKeys) {
|
||||||
return Q.resolve(collection.cassandraKeys);
|
return Q.resolve(collection.cassandraKeys);
|
||||||
}
|
}
|
||||||
|
@ -433,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
|
@ -442,6 +551,52 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||||
|
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<CassandraTableKey[]>();
|
||||||
|
|
||||||
|
$.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<CassandraTableKey[]> {
|
||||||
if (!!collection.cassandraSchema) {
|
if (!!collection.cassandraSchema) {
|
||||||
return Q.resolve(collection.cassandraSchema);
|
return Q.resolve(collection.cassandraSchema);
|
||||||
}
|
}
|
||||||
|
@ -473,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
deferred.resolve(data.columns);
|
deferred.resolve(data.columns);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||||
deferred.reject(error);
|
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
|
deferred.reject(errorText);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.done(clearInProgressMessage);
|
.done(clearInProgressMessage);
|
||||||
|
@ -482,6 +638,44 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||||
|
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<any> {
|
||||||
const deferred = Q.defer();
|
const deferred = Q.defer();
|
||||||
const { authType, databaseAccount } = userContext;
|
const { authType, databaseAccount } = userContext;
|
||||||
const apiEndpoint: string =
|
const apiEndpoint: string =
|
||||||
|
@ -547,4 +741,25 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||||
return collection.cassandraKeys.partitionKeys[0].property;
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.'
|
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 },
|
css: { placeholderVisible: filterContent().length === 0 },
|
||||||
textInput: filterContent"
|
textInput: filterContent,
|
||||||
|
event: { keydown: onFilterKeyDown }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist
|
<datalist
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
@ -84,9 +86,11 @@ export default class DocumentsTab extends TabsBase {
|
||||||
private _isQueryCopilotSampleContainer: boolean;
|
private _isQueryCopilotSampleContainer: boolean;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
private cancelQueryTimeoutID: NodeJS.Timeout;
|
private cancelQueryTimeoutID: NodeJS.Timeout;
|
||||||
|
private setKeyboardActions: KeyboardHandlerSetter;
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
||||||
|
|
||||||
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
||||||
|
@ -461,7 +465,22 @@ export default class DocumentsTab extends TabsBase {
|
||||||
|
|
||||||
private initializeNewDocument = (): void => {
|
private initializeNewDocument = (): void => {
|
||||||
this.selectedDocumentId(null);
|
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.initialDocumentContent(defaultDocument);
|
||||||
this.selectedDocumentContent.setBaseline(defaultDocument);
|
this.selectedDocumentContent.setBaseline(defaultDocument);
|
||||||
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
|
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
|
||||||
|
@ -648,9 +667,38 @@ export default class DocumentsTab extends TabsBase {
|
||||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
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<void> {
|
public async onActivate(): Promise<void> {
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
|
|
||||||
|
this.setKeyboardActions({
|
||||||
|
[KeyboardAction.SEARCH]: () => {
|
||||||
|
this.onShowFilterClick();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||||
|
this.filterContent("");
|
||||||
|
this.refreshDocumentsGrid(true);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!this._documentsIterator) {
|
if (!this._documentsIterator) {
|
||||||
try {
|
try {
|
||||||
await this.autoPopulateContent();
|
await this.autoPopulateContent();
|
||||||
|
@ -881,12 +929,18 @@ export default class DocumentsTab extends TabsBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
|
// All the following buttons require write access
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
|
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
|
||||||
if (this.newDocumentButton.visible()) {
|
if (this.newDocumentButton.visible()) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: NewDocumentIcon,
|
iconSrc: NewDocumentIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.NEW_ITEM,
|
||||||
onCommandClick: this.onNewDocumentClick,
|
onCommandClick: this.onNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -901,6 +955,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveNewDocumentClick,
|
onCommandClick: this.onSaveNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -915,6 +970,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onRevertNewDocumentClick,
|
onCommandClick: this.onRevertNewDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -930,6 +986,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.SAVE_ITEM,
|
||||||
onCommandClick: this.onSaveExistingDocumentClick,
|
onCommandClick: this.onSaveExistingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -944,6 +1001,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DiscardIcon,
|
iconSrc: DiscardIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.CANCEL_OR_DISCARD,
|
||||||
onCommandClick: this.onRevertExisitingDocumentClick,
|
onCommandClick: this.onRevertExisitingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
@ -959,6 +1017,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: DeleteDocumentIcon,
|
iconSrc: DeleteDocumentIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
keyboardAction: KeyboardAction.DELETE_ITEM,
|
||||||
onCommandClick: this.onDeleteExisitingDocumentClick,
|
onCommandClick: this.onDeleteExisitingDocumentClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue