Compare commits

...

14 Commits

Author SHA1 Message Date
Sung-Hyun Kang
3ae92f2398 testing icons 2025-01-14 17:16:02 -06:00
Sung-Hyun Kang
a0853c9167 testing 2025-01-14 14:45:19 -06:00
jawelton74
2fdb3df4ae Update to Nuget setup action v2. (#2024) 2025-01-10 17:32:12 -08:00
bogercraig
7c9802c07d Settings Menu Client Refresh Bug Fix, Limit Client Options in APIs (#2023)
* Reset hasDataPlaneRbacSettingChanged back to false after cosmos client is refreshed with new settings.
Dispose of old client before new one is created.

* Update client refresh variable after settings change.

* Only refresh client when related settings are changed.

* Update comparisons in settings menu.

* Remove unnecessary comments.

* Update refresh variable naming.

* Attempting to sync package.json and package-lock.json in CI.

* Remove npm install from CI after successful CI run.

* Only show retry settings with those APIs using the cosmos client -> NoSQL, Table, Gremlin
2025-01-10 12:42:03 -08:00
jawelton74
e5609bd91e Update Playwright to latest and rename MongoProxy development endpoint constant (#2022)
* Rename MongoProxy development endpoint constant to be consistent with other
endpoints.

* Update Playwright version to latest release due to test setup break.
2025-01-08 07:56:04 -08:00
jawelton74
4b75e86b74 Remove Network Warning banner from Data Explorer. (#2019) 2024-12-12 15:44:28 -08:00
SATYA SB
abf061089d [accessibility-3102896]: [Keyboard Navigation - Azure CosmosDB - Data Explorer]: "Learn more" link is not accessible using keyboard present inside the tooltip of "Analytical store’. (#1989)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-12-10 10:14:19 +05:30
Laurent Nguyen
ec25586a6e Close all tabs and always load first container when initializing Fabric (#2014)
Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2024-12-06 17:14:37 +01:00
tarazou9
c15d1432b2 Fix the filter for getting offering id (#2015)
Fix offering id filter
2024-12-05 13:04:46 -05:00
Laurent Nguyen
73d2686025 Restore open collection tabs: Query, Documents, Settings (#2004)
* Persist query multiple query texts

* Save multiple query tab histories

* Save and restore states for QueryTab and DocumentsTab for SQL and Mongo

* Enable Collection Scale/Settings restore

* Persist documents tab current filter

* Fix DocumentsTab conflict resolve mistake

* Remove unused variable

* Fix e2e test

* Fix e2e localStorage reference

* Try clearing local storage via playwright page

* Clear local storage after opening page

* Move restore flag behind feature flag. Whitelist restorable tabs in for Fabric. Restore e2e tests.

* Fix typo

* Fix: avoid setting undefined for preferredSize for the <Allotment.Pane>

* Add comments

* Move restore tabs after knockout configure step from Explorer constructor (which could be called multiple times)
2024-11-28 11:18:55 +01:00
vchske
80b926214b Vector Embedding and Full Text Search (#2009)
* Replaced monaco editor on Container Vector Policy tab with controls same as on create container ux

* Adds vector embedding policy to container management. Adds FullTextSearch to both add container and container management.

* Fixing unit tests and formatting issues

* More fixes

* Updating full text controls based on feedback

* Minor updates

* Editing test to fix compile issue

* Minor fix

* Adding paths for jest to ignore transform due to recent changes in upstream dependencies

* Adding mock to temporarily get unit tests to pass

* Hiding FTS feature behind the new EnableNoSQLFullTextSearch capability
2024-11-18 12:49:27 -08:00
Laurent Nguyen
070b7a4ca7 Remove unnecessary padding for Fabric (#2005)
Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2024-11-13 09:52:51 +01:00
jawelton74
d61ff5dcb5 Update upload/download-artifact actions to v4 due to v3 deprecation. (#2006) 2024-11-11 11:17:48 -08:00
Laurent Nguyen
d42eebaa5a Improve DocumentsTab filter input (#1998)
* Rework Input and dropdown in DocumentsTab

* Improve input: implement Escape and add clear button

* Undo body :focus outline, since fluent UI has a nicer focus style

* Close dropdown if last element is tabbed

* Fix unit tests

* Fix theme and remove autocomplete

* Load theme inside rendering function to fix using correct colors

* Remove commented code

* Add aria-label to clear filter button

* Fix format

* Fix keyboard navigation with tab and arrow up/down. Clear button becomes down button.

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2024-11-01 16:59:26 +01:00
67 changed files with 2515 additions and 1354 deletions

View File

@@ -92,7 +92,7 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=4096"
- run: cp -r ./Contracts ./dist/contracts
- run: cp -r ./configs ./dist/configs
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
@@ -113,18 +113,18 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps:
- uses: nuget/setup-nuget@v1
- uses: nuget/setup-nuget@v2
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: dist
- run: cp ./configs/prod.json config.json
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: packages
with:
path: "*.nupkg"
@@ -137,11 +137,11 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps:
- uses: nuget/setup-nuget@v1
- uses: nuget/setup-nuget@v2
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: dist
- run: cp ./configs/mpac.json config.json
@@ -149,7 +149,7 @@ jobs:
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: packages
with:
path: "*.nupkg"
@@ -185,9 +185,9 @@ jobs:
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-playwright-reports:
name: "Merge Playwright Reports"
@@ -197,26 +197,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14

View File

@@ -174,7 +174,11 @@ module.exports = {
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"],
transformIgnorePatterns: [
"/node_modules/(?!@fluentui/react-icons|(.*)/dist/browser)/",
"/node_modules/plotly.js-cartesian-dist-min",
"/externals/",
],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,

View File

@@ -26,7 +26,6 @@ a:focus {
#divExplorer {
background-color: #f5f5f5;
padding: @FabricBoxMargin;
}
.resourceTreeAndTabs {
@@ -38,12 +37,12 @@ a:focus {
}
.tabsManagerContainer {
background-color: #ffffff
background-color: #ffffff;
}
.nav-tabs-margin {
padding-top: 5px;
background-color: #ffffff
background-color: #ffffff;
}
.commandBarContainer {
@@ -68,17 +67,16 @@ a:focus {
}
}
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
border-bottom: 2px solid #e0e0e0;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
border-bottom: 2px solid @FabricAccentMedium;
}
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
border-bottom: 0px none transparent;
}
@@ -97,10 +95,10 @@ a:focus {
padding-bottom: @SmallSpace;
.contentWrapper {
.statusIconContainer {
margin-left: 0px;
}
.statusIconContainer {
margin-left: 0px;
}
}
.tabIconSection {
.cancelButton {
@@ -122,7 +120,6 @@ a:focus {
}
}
.resourceTree {
padding: 12px;
}
@@ -159,24 +156,21 @@ a:focus {
}
.selected {
&>.treeNodeHeader {
& > .treeNodeHeader {
background-color: @FabricAccentExtra;
}
}
}
}
.dataExplorerErrorConsoleContainer {
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin-top: 0px;
width: auto;
align-self: auto;
}
.filterbtnstyle {
background: #fff;
color: #000;
@@ -202,12 +196,10 @@ a:focus {
border: solid 1px #d1d1d1;
}
.gridRowSelected .tabdocumentsGridElement:hover {
background-color: @FabricAccentLight !important;
}
.refreshcol {
filter: brightness(0) saturate(100%);
}
@@ -218,4 +210,4 @@ a:focus {
.fileImportImg img {
filter: brightness(0) saturate(100%);
}
}

251
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
@@ -122,7 +122,7 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.44.0",
"@playwright/test": "1.49.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -228,18 +228,20 @@
}
},
"node_modules/@azure/abort-controller": {
"version": "1.1.0",
"license": "MIT",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
"dependencies": {
"tslib": "^2.2.0"
"tslib": "^2.6.2"
},
"engines": {
"node": ">=12.0.0"
"node": ">=18.0.0"
}
},
"node_modules/@azure/abort-controller/node_modules/tslib": {
"version": "2.6.2",
"license": "0BSD"
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/arm-cosmosdb": {
"version": "9.1.0",
@@ -251,15 +253,16 @@
}
},
"node_modules/@azure/core-auth": {
"version": "1.5.0",
"license": "MIT",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-util": "^1.1.0",
"tslib": "^2.2.0"
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.11.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-auth/node_modules/tslib": {
@@ -282,36 +285,61 @@
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-client/node_modules/@azure/abort-controller": {
"version": "2.1.2",
"license": "MIT",
"node_modules/@azure/core-client/node_modules/tslib": {
"version": "2.6.2",
"license": "0BSD"
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz",
"integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-client/node_modules/tslib": {
"version": "2.6.2",
"license": "0BSD"
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.12.2",
"license": "MIT",
"node_modules/@azure/core-rest-pipeline/node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.3.0",
"@azure/logger": "^1.0.0",
"form-data": "^4.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"tslib": "^2.2.0"
"debug": "^4.3.4"
},
"engines": {
"node": ">=16.0.0"
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/tslib": {
@@ -319,13 +347,14 @@
"license": "0BSD"
},
"node_modules/@azure/core-tracing": {
"version": "1.0.1",
"license": "MIT",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz",
"integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==",
"dependencies": {
"tslib": "^2.2.0"
"tslib": "^2.6.2"
},
"engines": {
"node": ">=12.0.0"
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-tracing/node_modules/tslib": {
@@ -333,14 +362,15 @@
"license": "0BSD"
},
"node_modules/@azure/core-util": {
"version": "1.6.1",
"license": "MIT",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"tslib": "^2.2.0"
"@azure/abort-controller": "^2.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16.0.0"
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-util/node_modules/tslib": {
@@ -348,22 +378,20 @@
"license": "0BSD"
},
"node_modules/@azure/cosmos": {
"version": "4.0.1-beta.3",
"license": "MIT",
"version": "4.2.0-beta.1",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz",
"integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-rest-pipeline": "^1.2.0",
"@azure/core-tracing": "^1.0.0",
"debug": "^4.1.1",
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.7.1",
"@azure/core-rest-pipeline": "^1.15.1",
"@azure/core-tracing": "^1.1.1",
"@azure/core-util": "^1.8.1",
"fast-json-stable-stringify": "^2.1.0",
"jsbi": "^3.1.3",
"node-abort-controller": "^3.0.0",
"jsbi": "^4.3.0",
"priorityqueuejs": "^2.0.0",
"semaphore": "^1.0.5",
"tslib": "^2.2.0",
"universal-user-agent": "^6.0.0",
"uuid": "^8.3.0"
"semaphore": "^1.1.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
@@ -10147,34 +10175,18 @@
}
},
"node_modules/@playwright/test": {
"version": "1.44.0",
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.0"
"playwright": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@playwright/test/node_modules/playwright": {
"version": "1.44.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
"node": ">=18"
}
},
"node_modules/@polka/url": {
@@ -11708,6 +11720,7 @@
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
@@ -14775,6 +14788,15 @@
"node": ">=8"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bintrees": {
"version": "1.0.2",
"license": "MIT"
@@ -19437,6 +19459,12 @@
"version": "2.0.5",
"license": "MIT"
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"node_modules/filesize": {
"version": "8.0.7",
"dev": true,
@@ -19895,6 +19923,7 @@
},
"node_modules/form-data": {
"version": "4.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -20024,6 +20053,19 @@
"version": "1.0.0",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"license": "MIT",
@@ -21095,6 +21137,7 @@
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@tootallnate/once": "2",
@@ -24129,6 +24172,24 @@
"fsevents": "^1.2.7"
}
},
"node_modules/jest-haste-map/node_modules/fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"deprecated": "Upgrade to fsevents v2 to mitigate potential security issues",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/jest-haste-map/node_modules/jest-worker": {
"version": "24.9.0",
"license": "MIT",
@@ -27067,8 +27128,9 @@
}
},
"node_modules/jsbi": {
"version": "3.2.5",
"license": "Apache-2.0"
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
},
"node_modules/jsbn": {
"version": "0.1.1",
@@ -29737,7 +29799,9 @@
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
"license": "MIT"
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/node-addon-api": {
"version": "4.3.0",
@@ -30764,15 +30828,34 @@
"node": ">=8"
}
},
"node_modules/playwright-core": {
"version": "1.44.0",
"node_modules/playwright": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"dependencies": {
"playwright-core": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/plotly.js-cartesian-dist-min": {

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
@@ -117,7 +117,7 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.44.0",
"@playwright/test": "1.49.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -170,10 +170,10 @@
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-circus": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2",
"jest-environment-jsdom": "29.7.0",
"less": "3.8.1",
"less-loader": "11.1.3",
"less-vars-loader": "1.1.0",
@@ -247,4 +247,4 @@
"printWidth": 120,
"endOfLine": "auto"
}
}
}

View File

@@ -89,6 +89,7 @@ export class CapabilityNames {
public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
}
export enum CapacityMode {
@@ -148,7 +149,7 @@ export class PortalBackendEndpoints {
}
export class MongoProxyEndpoints {
public static readonly Local: string = "https://localhost:7238";
public static readonly Development: 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";

View File

@@ -8,7 +8,7 @@ import { AuthType } from "../AuthType";
import { BackendApi, PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext";
import { userContext } from "../UserContext";
import { updateUserContext, userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@@ -189,10 +189,19 @@ let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient {
if (_client) {
if (!userContext.hasDataPlaneRbacSettingChanged) {
if (!userContext.refreshCosmosClient) {
return _client;
}
_client.dispose();
_client = null;
}
if (userContext.refreshCosmosClient) {
updateUserContext({
refreshCosmosClient: false,
});
}
let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;

View File

@@ -1,4 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
// import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../Contracts/ViewModels";
interface QueryResponse {
@@ -11,17 +11,13 @@ interface QueryResponse {
}
export interface MinimalQueryIterator {
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
fetchNext: () => Promise<QueryResponse>;
}
// Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
return documentsIterator.fetchNext().then((response) => {
const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -722,63 +722,63 @@ export function getEndpoint(endpoint: string): string {
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
[MongoProxyApi.ResourceList]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.QueryDocuments]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.CreateDocument]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.ReadDocument]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.UpdateDocument]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.DeleteDocument]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.CreateCollectionWithProxy]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.LegacyMongoShell]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.BulkDelete]: [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,

View File

@@ -99,6 +99,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
if (params.fullTextPolicy) {
resource.fullTextPolicy = params.fullTextPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: {
@@ -270,6 +273,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
analyticalStorageTtl: params.analyticalStorageTtl,
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
fullTextPolicy: params.fullTextPolicy,
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
const collectionOptions: RequestOptions = {};
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };

View File

@@ -1,4 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
// import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getEntityName } from "../DocumentUtility";
@@ -9,13 +9,13 @@ export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
// queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;

View File

@@ -9,6 +9,7 @@ export enum TabKind {
Graph,
SQLQuery,
ScaleSettings,
MongoQuery,
}
/**
@@ -51,6 +52,8 @@ export interface OpenCollectionTab extends OpenTab {
*/
export interface OpenQueryTab extends OpenCollectionTab {
query: QueryInfo;
splitterDirection?: "vertical" | "horizontal";
queryViewSizePercent?: number;
}
/**

View File

@@ -159,6 +159,7 @@ export interface Collection extends Resource {
analyticalStorageTtl?: number;
geospatialConfig?: GeospatialConfig;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
schema?: ISchema;
requestSchema?: () => void;
computedProperties?: ComputedProperties;
@@ -199,11 +200,19 @@ export interface IndexingPolicy {
compositeIndexes?: any[];
spatialIndexes?: any[];
vectorIndexes?: VectorIndex[];
fullTextIndexes?: FullTextIndex[];
}
export interface VectorIndex {
path: string;
type: "flat" | "diskANN" | "quantizedFlat";
diskANNShardKey?: string;
indexingSearchListSize?: number;
quantizationByteSize?: number;
}
export interface FullTextIndex {
path: string;
}
export interface ComputedProperty {
@@ -342,6 +351,7 @@ export interface CreateCollectionParams {
uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
}
export interface VectorEmbeddingPolicy {
@@ -355,6 +365,16 @@ export interface VectorEmbedding {
path: string;
}
export interface FullTextPolicy {
defaultLanguage: string;
fullTextPaths: FullTextPath[];
}
export interface FullTextPath {
path: string;
language: string;
}
export interface ReadDatabaseOfferParams {
databaseId: string;
databaseResourceId?: string;

View File

@@ -115,7 +115,13 @@ export interface CollectionBase extends TreeNode {
isSampleCollection?: boolean;
onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
onNewQueryClick(
source: any,
event?: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
): void;
expandCollection(): void;
collapseCollection(): void;
getDatabase(): Database;
@@ -126,6 +132,8 @@ export interface Collection extends CollectionBase {
analyticalStorageTtl: ko.Observable<number>;
schema?: DataModels.ISchema;
requestSchema?: () => void;
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
usageSizeInKB: ko.Observable<number>;
@@ -149,7 +157,13 @@ export interface Collection extends CollectionBase {
onSettingsClick: () => Promise<void>;
onNewGraphClick(): void;
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
onNewMongoQueryClick(
source: any,
event?: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
): void;
onNewMongoShellClick(): void;
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
@@ -309,6 +323,8 @@ export interface QueryTabOptions extends TabOptions {
partitionKey?: DataModels.PartitionKey;
queryText?: string;
resourceTokenPartitionKey?: string;
splitterDirection?: "horizontal" | "vertical";
queryViewSizePercent?: number;
}
export interface ScriptTabOption extends TabOptions {

View File

@@ -1,4 +1,4 @@
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
@@ -9,6 +9,9 @@ export interface CollapsibleSectionProps {
onExpand?: () => void;
children: JSX.Element;
tooltipContent?: string | JSX.Element | JSX.Element[];
showDelete?: boolean;
onDelete?: () => void;
disabled?: boolean;
}
export interface CollapsibleSectionState {
@@ -69,6 +72,20 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
)}
{this.props.showDelete && (
<Stack.Item style={{ marginLeft: "auto" }}>
<IconButton
disabled={this.props.disabled}
id={`delete-${this.props.title.split(" ").join("-")}`}
iconProps={{ iconName: "Delete" }}
style={{ height: 27, marginRight: "20px" }}
onClick={(event) => {
event.stopPropagation();
this.props.onDelete();
}}
/>
</Stack.Item>
)}
</Stack>
{this.state.isExpanded && this.props.children}
</>

View File

@@ -0,0 +1,6 @@
import "@testing-library/jest-dom";
describe("AddFullTextPolicyForm", () => {
//CTODO: add tests
it.skip("should render correctly", () => {});
});

View File

@@ -0,0 +1,239 @@
import {
DefaultButton,
Dropdown,
IDropdownOption,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
Label,
Stack,
TextField,
} from "@fluentui/react";
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import * as React from "react";
export interface FullTextPoliciesComponentProps {
fullTextPolicy: FullTextPolicy;
onFullTextPathChange: (
fullTextPolicy: FullTextPolicy,
fullTextIndexes: FullTextIndex[],
validationPassed: boolean,
) => void;
discardChanges?: boolean;
onChangesDiscarded?: () => void;
}
export interface FullTextPolicyData {
path: string;
language: string;
pathError: string;
}
const labelStyles = {
root: {
fontSize: 12,
},
};
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
fieldGroup: {
height: 27,
},
field: {
fontSize: 12,
padding: "0 8px",
},
};
const dropdownStyles = {
title: {
height: 27,
lineHeight: "24px",
fontSize: 12,
},
dropdown: {
height: 27,
lineHeight: "24px",
},
dropdownItem: {
fontSize: 12,
},
};
export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPoliciesComponentProps> = ({
fullTextPolicy,
onFullTextPathChange,
discardChanges,
onChangesDiscarded,
}): JSX.Element => {
const getFullTextPathError = (path: string, index?: number): string => {
let error = "";
if (!path) {
error = "Full text path should not be empty";
}
if (
index >= 0 &&
fullTextPathData?.find(
(fullTextPath: FullTextPolicyData, dataIndex: number) => dataIndex !== index && fullTextPath.path === path,
)
) {
error = "Full text path is already defined";
}
return error;
};
const initializeData = (fullTextPolicy: FullTextPolicy): FullTextPolicyData[] => {
if (!fullTextPolicy) {
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
}
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
...fullTextPath,
pathError: getFullTextPathError(fullTextPath.path),
}));
};
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never),
);
React.useEffect(() => {
propagateData();
}, [fullTextPathData, defaultLanguage]);
React.useEffect(() => {
if (discardChanges) {
setFullTextPathData(initializeData(fullTextPolicy));
setDefaultLanguage(fullTextPolicy.defaultLanguage);
onChangesDiscarded();
}
}, [discardChanges]);
const propagateData = () => {
const newFullTextPolicy: FullTextPolicy = {
defaultLanguage: defaultLanguage,
fullTextPaths: fullTextPathData.map((policy: FullTextPolicyData) => ({
path: policy.path,
language: policy.language,
})),
};
const fullTextIndexes: FullTextIndex[] = fullTextPathData.map((policy) => ({
path: policy.path,
}));
const validationPassed = fullTextPathData.every((policy: FullTextPolicyData) => policy.pathError === "");
onFullTextPathChange(newFullTextPolicy, fullTextIndexes, validationPassed);
};
const onFullTextPathValueChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
const fullTextPaths = [...fullTextPathData];
if (!fullTextPaths[index]?.path && !value.startsWith("/")) {
fullTextPaths[index].path = "/" + value;
} else {
fullTextPaths[index].path = value;
}
fullTextPaths[index].pathError = getFullTextPathError(value, index);
setFullTextPathData(fullTextPaths);
};
const onFullTextPathPolicyChange = (index: number, option: IDropdownOption): void => {
const policies = [...fullTextPathData];
policies[index].language = option.key as never;
setFullTextPathData(policies);
};
const onAdd = () => {
setFullTextPathData([
...fullTextPathData,
{
path: "",
language: defaultLanguage,
pathError: getFullTextPathError(""),
},
]);
};
const onDelete = (index: number) => {
const policies = fullTextPathData.filter((_uniqueKey, j) => index !== j);
setFullTextPathData(policies);
};
return (
<Stack tokens={{ childrenGap: 4 }}>
<Stack style={{ marginBottom: 10 }}>
<Label styles={labelStyles}>Default language</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getFullTextLanguageOptions()}
selectedKey={defaultLanguage}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
setDefaultLanguage(option.key as never)
}
></Dropdown>
</Stack>
{fullTextPathData &&
fullTextPathData.length > 0 &&
fullTextPathData.map((fullTextPolicy: FullTextPolicyData, index: number) => (
<CollapsibleSectionComponent
key={index}
isExpandedByDefault={true}
title={`Full text path ${index + 1}`}
showDelete={true}
onDelete={() => onDelete(index)}
>
<Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack
styles={{
root: {
margin: "0 0 6px 20px !important",
paddingLeft: 20,
width: "80%",
borderLeft: "1px solid",
},
}}
>
<Stack>
<Label styles={labelStyles}>Path</Label>
<TextField
id={`full-text-policy-path-${index + 1}`}
required={true}
placeholder="/fullTextPath1"
styles={textFieldStyles}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onFullTextPathValueChange(index, event)}
value={fullTextPolicy.path || ""}
errorMessage={fullTextPolicy.pathError}
/>
</Stack>
<Stack>
<Label styles={labelStyles}>Language</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getFullTextLanguageOptions()}
selectedKey={fullTextPolicy.language}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onFullTextPathPolicyChange(index, option)
}
></Dropdown>
</Stack>
</Stack>
</Stack>
</CollapsibleSectionComponent>
))}
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
Add full text path
</DefaultButton>
</Stack>
);
};
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
return [
{
key: "en-US",
text: "English (US)",
},
];
};

View File

@@ -0,0 +1,314 @@
// This component is used to create a dropdown list of options for the user to select from.
// The options are displayed in a dropdown list when the user clicks on the input field.
// The user can then select an option from the list. The selected option is then displayed in the input field.
import { getTheme } from "@fluentui/react";
import {
Button,
Divider,
Input,
Link,
makeStyles,
Popover,
PopoverProps,
PopoverSurface,
PositioningImperativeRef,
} from "@fluentui/react-components";
import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons";
import { NormalizedEventKey } from "Common/Constants";
import { tokens } from "Explorer/Theme/ThemeUtil";
import React, { FC, useEffect, useRef } from "react";
const useStyles = makeStyles({
container: {
padding: 0,
},
input: {
flexGrow: 1,
paddingRight: 0,
outline: "none",
"& input:focus": {
outline: "none", // Undo body :focus dashed outline
},
},
inputButton: {
border: 0,
},
dropdownHeader: {
width: "100%",
fontSize: tokens.fontSizeBase300,
fontWeight: 600,
padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`,
},
dropdownStack: {
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalS,
marginTop: tokens.spacingVerticalS,
marginBottom: "1px",
},
dropdownOption: {
fontSize: tokens.fontSizeBase300,
fontWeight: 400,
justifyContent: "left",
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
border: 0,
":hover": {
outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`,
backgroundColor: tokens.colorNeutralBackground2Hover,
color: tokens.colorNeutralForeground1,
},
},
bottomSection: {
fontSize: tokens.fontSizeBase300,
fontWeight: 400,
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
},
});
export interface InputDatalistDropdownOptionSection {
label: string;
options: string[];
}
export interface InputDataListProps {
dropdownOptions: InputDatalistDropdownOptionSection[];
placeholder?: string;
title?: string;
value: string;
onChange: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
autofocus?: boolean; // true: acquire focus on first render
bottomLink?: {
text: string;
url: string;
};
}
export const InputDataList: FC<InputDataListProps> = ({
dropdownOptions,
placeholder,
title,
value,
onChange,
onKeyDown,
autofocus,
bottomLink,
}) => {
const styles = useStyles();
const [showDropdown, setShowDropdown] = React.useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const positioningRef = React.useRef<PositioningImperativeRef>(null);
const [isInputFocused, setIsInputFocused] = React.useState(autofocus);
const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false);
const theme = getTheme();
const itemRefs = useRef([]);
useEffect(() => {
if (inputRef.current) {
positioningRef.current?.setTarget(inputRef.current);
}
}, [inputRef, positioningRef]);
useEffect(() => {
if (isInputFocused) {
inputRef.current?.focus();
}
}, [isInputFocused]);
useEffect(() => {
if (autofocusFirstDropdownItem && showDropdown) {
// Autofocus on first item if input isn't focused
itemRefs.current[0]?.focus();
setAutofocusFirstDropdownItem(false);
}
}, [autofocusFirstDropdownItem, showDropdown]);
const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => {
if (isInputFocused && !data.open) {
// Don't close if input is focused and we're opening the dropdown (which will steal the focus)
return;
}
setShowDropdown(data.open || false);
if (data.open) {
setIsInputFocused(true);
}
};
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === NormalizedEventKey.Escape) {
setShowDropdown(false);
} else if (e.key === NormalizedEventKey.DownArrow) {
setShowDropdown(true);
setAutofocusFirstDropdownItem(true);
}
onKeyDown(e);
};
const handleDownDropdownItemKeyDown = (
e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>,
index: number,
) => {
if (e.key === NormalizedEventKey.Enter) {
e.currentTarget.click();
} else if (e.key === NormalizedEventKey.Escape) {
setShowDropdown(false);
inputRef.current?.focus();
} else if (e.key === NormalizedEventKey.DownArrow) {
if (index + 1 < itemRefs.current.length) {
itemRefs.current[index + 1].focus();
} else {
setIsInputFocused(true);
}
} else if (e.key === NormalizedEventKey.UpArrow) {
if (index - 1 >= 0) {
itemRefs.current[index - 1].focus();
} else {
// Last item, focus back to input
setIsInputFocused(true);
}
}
};
// Flatten dropdownOptions to better manage refs and focus
let flatIndex = 0;
const indexMap = new Map<string, number>();
for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) {
const section = dropdownOptions[sectionIndex];
for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) {
indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex);
flatIndex++;
}
}
return (
<>
<Input
id="filterInput"
ref={inputRef}
type="text"
size="small"
autoComplete="off"
className={`filterInput ${styles.input}`}
title={title}
placeholder={placeholder}
value={value}
autoFocus
onKeyDown={handleInputKeyDown}
onChange={(e) => {
const newValue = e.target.value;
// Don't show dropdown if there is already a value in the input field (when user is typing)
setShowDropdown(!(newValue.length > 0));
onChange(newValue);
}}
onClick={(e) => {
e.stopPropagation();
}}
onFocus={() => {
// Don't show dropdown if there is already a value in the input field
// or isInputFocused is undefined which means component is mounting
setShowDropdown(!(value.length > 0) && isInputFocused !== undefined);
setIsInputFocused(true);
}}
onBlur={() => {
setIsInputFocused(false);
}}
contentAfter={
value.length > 0 ? (
<Button
aria-label="Clear filter"
className={styles.inputButton}
size="small"
icon={<DismissRegular />}
onClick={() => {
onChange("");
setIsInputFocused(true);
}}
/>
) : (
<Button
aria-label="Open dropdown"
className={styles.inputButton}
size="small"
icon={<ArrowDownRegular />}
onClick={() => {
setShowDropdown(true);
setAutofocusFirstDropdownItem(true);
}}
/>
)
}
/>
<Popover
inline
unstable_disableAutoFocus
// trapFocus
open={showDropdown}
onOpenChange={handleOpenChange}
positioning={{ positioningRef, position: "below", align: "start", offset: 4 }}
>
<PopoverSurface className={styles.container}>
{dropdownOptions.map((section, sectionIndex) => (
<div key={section.label}>
<div className={styles.dropdownHeader} style={{ color: theme.palette.themePrimary }}>
{section.label}
</div>
<div className={styles.dropdownStack}>
{section.options.map((option, index) => (
<Button
key={option}
ref={(el) => (itemRefs.current[indexMap.get(`${sectionIndex}-${index}`)] = el)}
appearance="transparent"
shape="square"
className={styles.dropdownOption}
onClick={() => {
onChange(option);
setShowDropdown(false);
setIsInputFocused(true);
}}
onBlur={() =>
!bottomLink &&
sectionIndex === dropdownOptions.length - 1 &&
index === section.options.length - 1 &&
setShowDropdown(false)
}
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) =>
handleDownDropdownItemKeyDown(e, indexMap.get(`${sectionIndex}-${index}`))
}
>
{option}
</Button>
))}
</div>
</div>
))}
{bottomLink && (
<>
<Divider />
<div className={styles.bottomSection}>
<Link
ref={(el) => (itemRefs.current[flatIndex] = el)}
href={bottomLink.url}
target="_blank"
onBlur={() => setShowDropdown(false)}
onKeyDown={(e: React.KeyboardEvent<HTMLAnchorElement>) => handleDownDropdownItemKeyDown(e, flatIndex)}
>
{bottomLink.text}
</Link>
</div>
</>
)}
</PopoverSurface>
</Popover>
</>
);
};

View File

@@ -4,11 +4,11 @@ import {
ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import {
ContainerVectorPolicyComponent,
ContainerVectorPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
ContainerPolicyComponent,
ContainerPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
import { useDatabases } from "Explorer/useDatabases";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -105,6 +105,13 @@ export interface SettingsComponentState {
isSubSettingsSaveable: boolean;
isSubSettingsDiscardable: boolean;
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
fullTextPolicy: DataModels.FullTextPolicy;
fullTextPolicyBaseline: DataModels.FullTextPolicy;
shouldDiscardContainerPolicies: boolean;
isContainerPolicyDirty: boolean;
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
shouldDiscardIndexingPolicy: boolean;
@@ -149,6 +156,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean;
private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource;
@@ -164,6 +172,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -203,6 +212,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
vectorEmbeddingPolicy: undefined,
vectorEmbeddingPolicyBaseline: undefined,
fullTextPolicy: undefined,
fullTextPolicyBaseline: undefined,
shouldDiscardContainerPolicies: false,
isContainerPolicyDirty: false,
indexingPolicyContent: undefined,
indexingPolicyContentBaseline: undefined,
shouldDiscardIndexingPolicy: false,
@@ -307,6 +323,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return (
this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
@@ -318,6 +335,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return (
this.state.isScaleDiscardable ||
this.state.isSubSettingsDiscardable ||
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
@@ -405,6 +423,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicyBaseline,
fullTextPolicy: this.state.fullTextPolicyBaseline,
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
indexesToAdd: [],
indexesToDrop: [],
@@ -416,11 +436,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
shouldDiscardContainerPolicies: true,
shouldDiscardIndexingPolicy: true,
isScaleSaveable: false,
isScaleDiscardable: false,
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isContainerPolicyDirty: false,
isIndexingPolicyDirty: false,
isMongoIndexingPolicySaveable: false,
isMongoIndexingPolicyDiscardable: false,
@@ -448,9 +470,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
this.setState({ isScaleDiscardable: isScaleDiscardable });
private onVectorEmbeddingPolicyChange = (newVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy): void =>
this.setState({ vectorEmbeddingPolicy: newVectorEmbeddingPolicy });
private onFullTextPolicyChange = (newFullTextPolicy: DataModels.FullTextPolicy): void =>
this.setState({ fullTextPolicy: newFullTextPolicy });
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy });
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
private logIndexingPolicySuccessMessage = (): void => {
@@ -538,6 +568,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void =>
this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty });
private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void =>
this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty });
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
@@ -691,6 +727,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off;
const vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy =
this.collection.vectorEmbeddingPolicy && this.collection.vectorEmbeddingPolicy();
const fullTextPolicy: DataModels.FullTextPolicy =
this.collection.fullTextPolicy && this.collection.fullTextPolicy();
const indexingPolicyContent = this.collection.indexingPolicy();
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
@@ -724,6 +764,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
fullTextPolicy: fullTextPolicy,
fullTextPolicyBaseline: fullTextPolicy,
indexingPolicyContent: indexingPolicyContent,
indexingPolicyContentBaseline: indexingPolicyContent,
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
@@ -854,6 +898,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (
this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty
@@ -875,6 +920,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
newCollection.defaultTtl = defaultTtl;
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
newCollection.fullTextPolicy = this.state.fullTextPolicy;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
newCollection.changeFeedPolicy =
@@ -913,6 +962,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
this.collection.computedProperties(updatedCollection.computedProperties);
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress();
@@ -921,6 +972,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isContainerPolicyDirty: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false,
isComputedPropertiesDirty: false,
@@ -1091,6 +1143,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
};
const containerPolicyComponentProps: ContainerPolicyComponentProps = {
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicy,
vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline,
onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange,
onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange,
isVectorSearchEnabled: this.isVectorSearchEnabled,
fullTextPolicy: this.state.fullTextPolicy,
fullTextPolicyBaseline: this.state.fullTextPolicyBaseline,
onFullTextPolicyChange: this.onFullTextPolicyChange,
onFullTextPolicyDirtyChange: this.onFullTextPolicyDirtyChange,
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
};
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
@@ -1148,10 +1215,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
explorer: this.props.settingsTab.getContainer(),
};
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
};
const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({
@@ -1165,10 +1228,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <SubSettingsComponent {...subSettingsComponentProps} />,
});
if (this.isVectorSearchEnabled) {
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) {
tabs.push({
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />,
});
}

View File

@@ -0,0 +1,6 @@
import "@testing-library/jest-dom";
describe("ContainerPolicyComponent", () => {
//CTODO: add tests
it.skip("should render correctly", () => {});
});

View File

@@ -0,0 +1,163 @@
import { DefaultButton, Pivot, PivotItem, Stack } from "@fluentui/react";
import { FullTextPolicy, VectorEmbedding, VectorEmbeddingPolicy } from "Contracts/DataModels";
import {
FullTextPoliciesComponent,
getFullTextLanguageOptions,
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import React from "react";
export interface ContainerPolicyComponentProps {
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
vectorEmbeddingPolicyBaseline: VectorEmbeddingPolicy;
onVectorEmbeddingPolicyChange: (newVectorEmbeddingPolicy: VectorEmbeddingPolicy) => void;
onVectorEmbeddingPolicyDirtyChange: (isVectorEmbeddingPolicyDirty: boolean) => void;
isVectorSearchEnabled: boolean;
fullTextPolicy: FullTextPolicy;
fullTextPolicyBaseline: FullTextPolicy;
onFullTextPolicyChange: (newFullTextPolicy: FullTextPolicy) => void;
onFullTextPolicyDirtyChange: (isFullTextPolicyDirty: boolean) => void;
isFullTextSearchEnabled: boolean;
shouldDiscardContainerPolicies: boolean;
resetShouldDiscardContainerPolicyChange: () => void;
}
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
vectorEmbeddingPolicy,
vectorEmbeddingPolicyBaseline,
onVectorEmbeddingPolicyChange,
onVectorEmbeddingPolicyDirtyChange,
isVectorSearchEnabled,
fullTextPolicy,
fullTextPolicyBaseline,
onFullTextPolicyChange,
onFullTextPolicyDirtyChange,
isFullTextSearchEnabled,
shouldDiscardContainerPolicies,
resetShouldDiscardContainerPolicyChange,
}) => {
const [selectedTab, setSelectedTab] = React.useState<ContainerPolicyTabTypes>(
ContainerPolicyTabTypes.VectorPolicyTab,
);
const [vectorEmbeddings, setVectorEmbeddings] = React.useState<VectorEmbedding[]>();
const [vectorEmbeddingsBaseline, setVectorEmbeddingsBaseline] = React.useState<VectorEmbedding[]>();
const [discardVectorChanges, setDiscardVectorChanges] = React.useState<boolean>(false);
const [fullTextSearchPolicy, setFullTextSearchPolicy] = React.useState<FullTextPolicy>();
const [fullTextSearchPolicyBaseline, setFullTextSearchPolicyBaseline] = React.useState<FullTextPolicy>();
const [discardFullTextChanges, setDiscardFullTextChanges] = React.useState<boolean>(false);
React.useEffect(() => {
setVectorEmbeddings(vectorEmbeddingPolicy?.vectorEmbeddings);
setVectorEmbeddingsBaseline(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
}, [vectorEmbeddingPolicy]);
React.useEffect(() => {
setFullTextSearchPolicy(fullTextPolicy);
setFullTextSearchPolicyBaseline(fullTextPolicyBaseline);
}, [fullTextPolicy, fullTextPolicyBaseline]);
React.useEffect(() => {
if (shouldDiscardContainerPolicies) {
setVectorEmbeddings(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
setDiscardVectorChanges(true);
setFullTextSearchPolicy(fullTextPolicyBaseline);
setDiscardFullTextChanges(true);
resetShouldDiscardContainerPolicyChange();
}
});
const checkAndSendVectorEmbeddingPoliciesToSettings = (newVectorEmbeddings: VectorEmbedding[]): void => {
if (isDirty(newVectorEmbeddings, vectorEmbeddingsBaseline)) {
onVectorEmbeddingPolicyDirtyChange(true);
onVectorEmbeddingPolicyChange({ vectorEmbeddings: newVectorEmbeddings });
} else {
resetShouldDiscardContainerPolicyChange();
}
};
const checkAndSendFullTextPolicyToSettings = (newFullTextPolicy: FullTextPolicy): void => {
if (isDirty(newFullTextPolicy, fullTextSearchPolicyBaseline)) {
onFullTextPolicyDirtyChange(true);
onFullTextPolicyChange(newFullTextPolicy);
} else {
resetShouldDiscardContainerPolicyChange();
}
};
const onVectorChangesDiscarded = (): void => {
setDiscardVectorChanges(false);
};
const onFullTextChangesDiscarded = (): void => {
setDiscardFullTextChanges(false);
};
const onPivotChange = (item: PivotItem): void => {
const selectedTab = ContainerPolicyTabTypes[item.props.itemKey as keyof typeof ContainerPolicyTabTypes];
setSelectedTab(selectedTab);
};
return (
<div>
<Pivot onLinkClick={onPivotChange} selectedKey={ContainerPolicyTabTypes[selectedTab]}>
{isVectorSearchEnabled && (
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
style={{ marginTop: 20 }}
headerText="Vector Policy"
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{vectorEmbeddings && (
<VectorEmbeddingPoliciesComponent
disabled={true}
vectorEmbeddings={vectorEmbeddings}
vectorIndexes={undefined}
onVectorEmbeddingChange={(vectorEmbeddings: VectorEmbedding[]) =>
checkAndSendVectorEmbeddingPoliciesToSettings(vectorEmbeddings)
}
discardChanges={discardVectorChanges}
onChangesDiscarded={onVectorChangesDiscarded}
/>
)}
</Stack>
</PivotItem>
)}
{isFullTextSearchEnabled && (
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
style={{ marginTop: 20 }}
headerText="Full Text Policy"
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{fullTextSearchPolicy ? (
<FullTextPoliciesComponent
fullTextPolicy={fullTextSearchPolicy}
onFullTextPathChange={(newFullTextPolicy: FullTextPolicy) =>
checkAndSendFullTextPolicyToSettings(newFullTextPolicy)
}
discardChanges={discardFullTextChanges}
onChangesDiscarded={onFullTextChangesDiscarded}
/>
) : (
<DefaultButton
id={"create-full-text-policy"}
styles={{ root: { fontSize: 12 } }}
onClick={() => {
checkAndSendFullTextPolicyToSettings({
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
fullTextPaths: [],
});
}}
>
Create new full text search policy
</DefaultButton>
)}
</Stack>
</PivotItem>
)}
</Pivot>
</div>
);
};

View File

@@ -1,30 +0,0 @@
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>
);
};

View File

@@ -120,11 +120,6 @@ export class IndexingPolicyComponent extends React.Component<
indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{this.props.isVectorSearchEnabled && (
<MessageBar messageBarType={MessageBarType.severeWarning}>
Container vector policies and vector indexes are not modifiable after container creation
</MessageBar>
)}
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)}

View File

@@ -4,7 +4,14 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
export type isDirtyTypes =
| boolean
| string
| number
| DataModels.IndexingPolicy
| DataModels.ComputedProperties
| DataModels.VectorEmbedding[]
| DataModels.FullTextPolicy;
export const TtlOff = "off";
export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault";
@@ -50,6 +57,11 @@ export enum SettingsV2TabTypes {
ContainerVectorPolicyTab,
}
export enum ContainerPolicyTabTypes {
VectorPolicyTab,
FullTextPolicyTab,
}
export interface IsComponentDirtyResult {
isSaveable: boolean;
isDiscardable: boolean;
@@ -154,7 +166,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Vector Policy (preview)";
return "Container Policies";
default:
throw new Error(`Unknown tab ${tab}`);
}

View File

@@ -46,6 +46,8 @@ export const collection = {
query: "query",
},
]),
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
readSettings: () => {
return;
},

View File

@@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = `
},
"databaseId": "test",
"defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
@@ -71,6 +72,7 @@ exports[`SettingsComponent renders 1`] = `
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
isAutoPilotSelected={false}
@@ -132,6 +134,7 @@ exports[`SettingsComponent renders 1`] = `
},
"databaseId": "test",
"defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
@@ -148,6 +151,7 @@ exports[`SettingsComponent renders 1`] = `
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
displayedTtlSeconds="5"
@@ -249,6 +253,7 @@ exports[`SettingsComponent renders 1`] = `
},
"databaseId": "test",
"defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
@@ -265,6 +270,7 @@ exports[`SettingsComponent renders 1`] = `
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
explorer={

View File

@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import React from "react";
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
const mockVectorEmbedding: VectorEmbedding[] = [
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
beforeEach(() => {
component = render(
<AddVectorEmbeddingPolicyForm
vectorEmbedding={mockVectorEmbedding}
vectorIndex={mockVectorIndex}
<VectorEmbeddingPoliciesComponent
vectorEmbeddings={mockVectorEmbedding}
vectorIndexes={mockVectorIndex}
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
/>,
);
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
});
test("calls onDelete when delete button is clicked", async () => {
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
const deleteButton = component.container.querySelector("#delete-Vector-embedding-1");
fireEvent.click(deleteButton);
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
expect(screen.queryByText("Vector embedding 1")).toBeNull();
@@ -49,21 +49,19 @@ describe("AddVectorEmbeddingPolicyForm", () => {
test("validates input correctly", async () => {
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
await waitFor(() => expect(screen.getByText("Path should not be empty")).toBeInTheDocument(), {
timeout: 1500,
});
await waitFor(
() =>
expect(
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
).toBeInTheDocument(),
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(),
{
timeout: 1500,
},
);
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), {
timeout: 1500,
});
await waitFor(

View File

@@ -0,0 +1,470 @@
import {
DefaultButton,
Dropdown,
IDropdownOption,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
Label,
Stack,
TextField,
} from "@fluentui/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import {
getDataTypeOptions,
getDistanceFunctionOptions,
getIndexTypeOptions,
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
import React, { FunctionComponent, useState } from "react";
export interface IVectorEmbeddingPoliciesComponentProps {
vectorEmbeddings: VectorEmbedding[];
onVectorEmbeddingChange: (
vectorEmbeddings: VectorEmbedding[],
vectorIndexingPolicies: VectorIndex[],
validationPassed: boolean,
) => void;
vectorIndexes?: VectorIndex[];
discardChanges?: boolean;
onChangesDiscarded?: () => void;
disabled?: boolean;
}
export interface VectorEmbeddingPolicyData {
path: string;
dataType: VectorEmbedding["dataType"];
distanceFunction: VectorEmbedding["distanceFunction"];
dimensions: number;
indexType: VectorIndex["type"] | "none";
pathError: string;
dimensionsError: string;
diskANNShardKey?: string;
diskANNShardKeyError?: string;
indexingSearchListSize?: number;
indexingSearchListSizeError?: string;
quantizationByteSize?: number;
quantizationByteSizeError?: string;
}
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
const labelStyles = {
root: {
fontSize: 12,
},
};
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
fieldGroup: {
height: 27,
},
field: {
fontSize: 12,
padding: "0 8px",
},
};
const dropdownStyles = {
title: {
height: 27,
lineHeight: "24px",
fontSize: 12,
},
dropdown: {
height: 27,
lineHeight: "24px",
},
dropdownItem: {
fontSize: 12,
},
};
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
vectorEmbeddings,
vectorIndexes,
onVectorEmbeddingChange,
discardChanges,
onChangesDiscarded,
disabled,
}): JSX.Element => {
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
let error = "";
if (!path) {
error = "Path should not be empty";
}
if (
index >= 0 &&
vectorEmbeddingPolicyData?.find(
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
dataIndex !== index && vectorEmbedding.path === path,
)
) {
error = "Path is already defined";
}
return error;
};
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
let error = "";
if (dimension <= 0 || dimension > 4096) {
error = "Dimension must be greater than 0 and less than or equal 4096";
}
if (indexType === "flat" && dimension > 505) {
error = "Maximum allowed dimension for flat index is 505";
}
return error;
};
const onQuantizationByteSizeError = (size: number): string => {
let error = "";
if (size < 1 || size > 512) {
error = "Quantization byte size must be greater than 0 and less than or equal to 512";
}
return error;
};
const onIndexingSearchListSizeError = (size: number): string => {
let error = "";
if (size < 25 || size > 500) {
error = "Indexing search list size must be greater than or equal to 25 and less than or equal to 500";
}
return error;
};
//TODO: no restrictions yet due to this field being removed for now.
// Uncomment and replace with validation code when field is reinstated
// const onDiskANNShardKeyError = (shardKey: string): string => {
// return "";
// };
const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => {
const mergedData: VectorEmbeddingPolicyData[] = [];
vectorEmbeddings.forEach((embedding) => {
const matchingIndex = displayIndexes ? vectorIndexes.find((index) => index.path === embedding.path) : undefined;
mergedData.push({
...embedding,
indexType: matchingIndex?.type || "none",
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
pathError: onVectorEmbeddingPathError(embedding.path),
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
});
});
return mergedData;
};
const [displayIndexes] = useState<boolean>(!!vectorIndexes);
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
initializeData(vectorEmbeddings, vectorIndexes),
);
React.useEffect(() => {
propagateData();
}, [vectorEmbeddingPolicyData]);
React.useEffect(() => {
if (discardChanges) {
setVectorEmbeddingPolicyData(initializeData(vectorEmbeddings, vectorIndexes));
onChangesDiscarded();
}
}, [discardChanges]);
const propagateData = () => {
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
path: policy.path,
dataType: policy.dataType,
dimensions: policy.dimensions,
distanceFunction: policy.distanceFunction,
}));
const vectorIndexes: VectorIndex[] = vectorEmbeddingPolicyData
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
.map(
(policy) =>
({
path: policy.path,
type: policy.indexType,
indexingSearchListSize: policy.indexingSearchListSize,
quantizationByteSize: policy.quantizationByteSize,
}) as VectorIndex,
);
const validationPassed = vectorEmbeddingPolicyData.every(
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
);
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
};
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
vectorEmbeddings[index].path = "/" + value;
} else {
vectorEmbeddings[index].path = value;
}
const error = onVectorEmbeddingPathError(value, index);
vectorEmbeddings[index].pathError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value.trim()) || 0;
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
const vectorEmbedding = vectorEmbeddings[index];
vectorEmbeddings[index].dimensions = value;
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
vectorEmbeddings[index].dimensionsError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
const vectorEmbedding = vectorEmbeddings[index];
vectorEmbeddings[index].indexType = option.key as never;
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
vectorEmbeddings[index].dimensionsError = error;
if (vectorEmbedding.indexType === "diskANN") {
vectorEmbedding.indexingSearchListSize = 100;
} else {
vectorEmbedding.indexingSearchListSize = undefined;
}
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onQuantizationByteSizeChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value.trim()) || 0;
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
vectorEmbeddings[index].quantizationByteSize = value;
vectorEmbeddings[index].quantizationByteSizeError = onQuantizationByteSizeError(value);
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onIndexingSearchListSizeChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value.trim()) || 0;
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
vectorEmbeddings[index].indexingSearchListSize = value;
vectorEmbeddings[index].indexingSearchListSizeError = onIndexingSearchListSizeError(value);
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
// TODO: uncomment after Ignite
// DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
// const onDiskANNShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value.trim();
// const vectorEmbeddings = [...vectorEmbeddingPolicyData];
// if (!vectorEmbeddings[index]?.diskANNShardKey && !value.startsWith("/")) {
// vectorEmbeddings[index].diskANNShardKey = "/" + value;
// } else {
// vectorEmbeddings[index].diskANNShardKey = value;
// }
// const error = onDiskANNShardKeyError(value);
// vectorEmbeddings[index].diskANNShardKeyError = error;
// setVectorEmbeddingPolicyData(vectorEmbeddings);
// }
const onVectorEmbeddingPolicyChange = (
index: number,
option: IDropdownOption,
property: VectorEmbeddingPolicyProperty,
): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
vectorEmbeddings[index][property] = option.key as never;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onAdd = () => {
setVectorEmbeddingPolicyData([
...vectorEmbeddingPolicyData,
{
path: "",
dataType: "float32",
distanceFunction: "euclidean",
dimensions: 0,
indexType: "none",
pathError: onVectorEmbeddingPathError(""),
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
},
]);
};
const onDelete = (index: number) => {
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
return (
<Stack tokens={{ childrenGap: 4 }}>
{vectorEmbeddingPolicyData &&
vectorEmbeddingPolicyData.length > 0 &&
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
<CollapsibleSectionComponent
disabled={disabled}
key={index}
isExpandedByDefault={true}
title={`Vector embedding ${index + 1}`}
showDelete={true}
onDelete={() => onDelete(index)}
>
<Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack
styles={{
root: {
margin: "0 0 6px 20px !important",
paddingLeft: 20,
width: "80%",
borderLeft: "1px solid",
},
}}
>
<Stack>
<Label disabled={disabled} styles={labelStyles}>
Path
</Label>
<TextField
disabled={disabled}
id={`vector-policy-path-${index + 1}`}
required={true}
placeholder="/vector1"
styles={textFieldStyles}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
value={vectorEmbeddingPolicy.path || ""}
errorMessage={vectorEmbeddingPolicy.pathError}
/>
</Stack>
<Stack>
<Label disabled={disabled} styles={labelStyles}>
Data type
</Label>
<Dropdown
disabled={disabled}
required={true}
styles={dropdownStyles}
options={getDataTypeOptions()}
selectedKey={vectorEmbeddingPolicy.dataType}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingPolicyChange(index, option, "dataType")
}
></Dropdown>
</Stack>
<Stack>
<Label disabled={disabled} styles={labelStyles}>
Distance function
</Label>
<Dropdown
disabled={disabled}
required={true}
styles={dropdownStyles}
options={getDistanceFunctionOptions()}
selectedKey={vectorEmbeddingPolicy.distanceFunction}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
}
></Dropdown>
</Stack>
<Stack>
<Label disabled={disabled} styles={labelStyles}>
Dimensions
</Label>
<TextField
disabled={disabled}
id={`vector-policy-dimension-${index + 1}`}
required={true}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.dimensions || 0)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onVectorEmbeddingDimensionsChange(index, event)
}
errorMessage={vectorEmbeddingPolicy.dimensionsError}
/>
</Stack>
{displayIndexes && (
<Stack>
<Label disabled={disabled} styles={labelStyles}>
Index type
</Label>
<Dropdown
disabled={disabled}
required={true}
styles={dropdownStyles}
options={getIndexTypeOptions()}
selectedKey={vectorEmbeddingPolicy.indexType}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingIndexTypeChange(index, option)
}
></Dropdown>
<Stack style={{ marginLeft: "10px" }}>
<Label
disabled={
disabled ||
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
vectorEmbeddingPolicy.indexType !== "diskANN")
}
styles={labelStyles}
>
Quantization byte size
</Label>
<TextField
disabled={
disabled ||
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
vectorEmbeddingPolicy.indexType !== "diskANN")
}
id={`vector-policy-quantizationByteSize-${index + 1}`}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.quantizationByteSize || "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onQuantizationByteSizeChange(index, event)
}
/>
</Stack>
<Stack style={{ marginLeft: "10px" }}>
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}>
Indexing search list size
</Label>
<TextField
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
id={`vector-policy-indexingSearchListSize-${index + 1}`}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.indexingSearchListSize || "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onIndexingSearchListSizeChange(index, event)
}
/>
</Stack>
{/*TODO: uncomment after Ignite */}
{/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
<Stack
style={{ marginLeft: "10px" }}
>
<Label
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
styles={labelStyles}
>DiskANN shard key</Label>
<TextField
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
id={`vector-policy-diskANNShardKey-${index + 1}`}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.diskANNShardKey || "")}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onDiskANNShardKeyChange(index, event)
}
/>
</Stack>
*/}
</Stack>
)}
</Stack>
</Stack>
</CollapsibleSectionComponent>
))}
<DefaultButton
disabled={disabled}
id={`add-vector-policy`}
styles={{ root: { maxWidth: 170, fontSize: 12 } }}
onClick={onAdd}
>
Add vector embedding
</DefaultButton>
</Stack>
);
};

View File

@@ -1134,7 +1134,7 @@ export default class Explorer {
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
}
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();

View File

@@ -1,4 +1,5 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { configContext, Platform } from "ConfigContext";
import { useDatabases } from "Explorer/useDatabases";
import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts";
@@ -56,6 +57,19 @@ function openCollectionTab(
continue;
}
if (
configContext.platform === Platform.Fabric &&
!(
// whitelist the tab kinds that are allowed to be opened in Fabric
(
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery
)
)
) {
continue;
}
//expand database first if not expanded to load the collections
if (!database.isDatabaseExpanded?.()) {
database.expandDatabase?.();
@@ -121,10 +135,28 @@ function openCollectionTab(
action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) {
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
collection.onNewQueryClick(
collection,
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
openQueryTabAction.splitterDirection,
openQueryTabAction.queryViewSizePercent,
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.MongoQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
) {
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
collection.onNewMongoQueryClick(
collection,
undefined,
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
openQueryTabAction.splitterDirection,
openQueryTabAction.queryViewSizePercent,
);
break;
}

View File

@@ -21,7 +21,11 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
import {
FullTextPoliciesComponent,
getFullTextLanguageOptions,
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react";
@@ -30,7 +34,12 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import {
isCapabilityEnabled,
isFullTextSearchEnabled,
isServerlessAccount,
isVectorSearchEnabled,
} from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -109,6 +118,9 @@ export interface AddCollectionPanelState {
vectorIndexingPolicy: DataModels.VectorIndex[];
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
vectorPolicyValidated: boolean;
fullTextPolicy: DataModels.FullTextPolicy;
fullTextIndexes: DataModels.FullTextIndex[];
fullTextPolicyValidated: boolean;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -147,6 +159,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [],
vectorPolicyValidated: true,
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
fullTextIndexes: [],
fullTextPolicyValidated: true,
};
}
@@ -804,22 +819,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing">
<Stack horizontal>
<Text className="panelTextBold" variant="small">
Analytical store
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getAnalyticalStorageTooltipContent()}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
/>
</TooltipHost>
</Stack>
<Text className="panelTextBold" variant="small">
{this.getAnalyticalStorageContent()}
</Text>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
@@ -890,9 +892,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<AddVectorEmbeddingPolicyForm
vectorEmbedding={this.state.vectorEmbeddingPolicy}
vectorIndex={this.state.vectorIndexingPolicy}
<VectorEmbeddingPoliciesComponent
vectorEmbeddings={this.state.vectorEmbeddingPolicy}
vectorIndexes={this.state.vectorIndexingPolicy}
onVectorEmbeddingChange={(
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
vectorIndexingPolicy: DataModels.VectorIndex[],
@@ -906,6 +908,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent>
</Stack>
)}
{this.shouldShowFullTextSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleFullTextPolicySectionContent");
}}
//TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
>
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<FullTextPoliciesComponent
fullTextPolicy={this.state.fullTextPolicy}
onFullTextPathChange={(
fullTextPolicy: DataModels.FullTextPolicy,
fullTextIndexes: DataModels.FullTextIndex[],
fullTextPolicyValidated: boolean,
) => {
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
)}
{userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
@@ -1187,7 +1217,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return "";
}
private getAnalyticalStorageTooltipContent(): JSX.Element {
private getAnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without
@@ -1211,6 +1241,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
);
}
//TODO: uncomment when learn more text becomes available
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
// return (
// <Text variant="small">
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
// magna aliqua.{" "}
// <Link target="_blank" href="https://aka.ms/CosmosFullTextSearch">
// Learn more
// </Link>
// </Text>
// );
// }
private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) {
return false;
@@ -1274,6 +1317,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private shouldShowFullTextSearchParameters() {
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
if (this.state.uniqueKeys?.length === 0) {
return undefined;
@@ -1330,9 +1377,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
if (this.shouldShowVectorSearchParameters()) {
if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
}
if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
return false;
}
}
return true;
@@ -1423,6 +1477,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
};
}
if (this.shouldShowFullTextSearchParameters()) {
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
}
const telemetryData = {
database: {
id: databaseId,
@@ -1482,6 +1540,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
uniqueKeyPolicy,
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy,
fullTextPolicy: this.state.fullTextPolicy,
};
this.setState({ isExecuting: true });

View File

@@ -193,6 +193,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
if (
enableDataPlaneRBACOption !== LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ||
retryAttempts !== LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ||
retryInterval !== LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval) ||
MaxWaitTimeInSeconds !== LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
) {
updateUserContext({
refreshCosmosClient: true,
});
}
if (configContext.platform !== Platform.Fabric) {
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
if (
@@ -202,7 +213,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
) {
updateUserContext({
dataPlaneRbacEnabled: true,
hasDataPlaneRbacSettingChanged: true,
});
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
try {
@@ -226,7 +236,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
} else {
updateUserContext({
dataPlaneRbacEnabled: false,
hasDataPlaneRbacSettingChanged: true,
});
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
@@ -564,7 +573,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && (
<>
<AccordionItem value="3">
@@ -663,78 +671,79 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
</>
)}
<AccordionItem value="6">
<AccordionHeader>
<div className={styles.header}>Retry Settings</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Retry policy associated with throttled requests during CosmosDB queries.
{(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && (
<AccordionItem value="6">
<AccordionHeader>
<div className={styles.header}>Retry Settings</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Retry policy associated with throttled requests during CosmosDB queries.
</div>
<div>
<span className={styles.subHeader}>Max retry attempts</span>
<InfoTooltip className={styles.headerIcon}>
Max number of retries to be performed for a request. Default value 9.
</InfoTooltip>
</div>
<SpinButton
labelPosition={Position.top}
min={1}
step={1}
value={"" + retryAttempts}
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
styles={spinButtonStyles}
/>
<div>
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
<InfoTooltip className={styles.headerIcon}>
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
as part of the response. Default value is 0 milliseconds.
</InfoTooltip>
</div>
<SpinButton
labelPosition={Position.top}
min={1000}
step={1000}
value={"" + retryInterval}
onChange={handleOnRetryIntervalSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
styles={spinButtonStyles}
/>
<div>
<span className={styles.subHeader}>Max wait time (s)</span>
<InfoTooltip className={styles.headerIcon}>
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
seconds.
</InfoTooltip>
</div>
<SpinButton
labelPosition={Position.top}
min={1}
step={1}
value={"" + MaxWaitTimeInSeconds}
onChange={handleOnMaxWaitTimeSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
styles={spinButtonStyles}
/>
</div>
<div>
<span className={styles.subHeader}>Max retry attempts</span>
<InfoTooltip className={styles.headerIcon}>
Max number of retries to be performed for a request. Default value 9.
</InfoTooltip>
</div>
<SpinButton
labelPosition={Position.top}
min={1}
step={1}
value={"" + retryAttempts}
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
styles={spinButtonStyles}
/>
<div>
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
<InfoTooltip className={styles.headerIcon}>
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
part of the response. Default value is 0 milliseconds.
</InfoTooltip>
</div>
<SpinButton
labelPosition={Position.top}
min={1000}
step={1000}
value={"" + retryInterval}
onChange={handleOnRetryIntervalSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
styles={spinButtonStyles}
/>
<div>
<span className={styles.subHeader}>Max wait time (s)</span>
<InfoTooltip className={styles.headerIcon}>
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
seconds.
</InfoTooltip>
</div>
<SpinButton
labelPosition={Position.top}
min={1}
step={1}
value={"" + MaxWaitTimeInSeconds}
onChange={handleOnMaxWaitTimeSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
styles={spinButtonStyles}
/>
</div>
</AccordionPanel>
</AccordionItem>
</AccordionPanel>
</AccordionItem>
)}
<AccordionItem value="7">
<AccordionHeader>
@@ -758,7 +767,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div>
</AccordionPanel>
</AccordionItem>
{shouldShowCrossPartitionOption && (
<AccordionItem value="8">
<AccordionHeader>
@@ -784,7 +792,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowParallelismOption && (
<AccordionItem value="9">
<AccordionHeader>
@@ -818,7 +825,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowPriorityLevelOption && (
<AccordionItem value="10">
<AccordionHeader>
@@ -842,7 +848,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowGraphAutoVizOption && (
<AccordionItem value="11">
<AccordionHeader>
@@ -864,7 +869,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowCopilotSampleDBOption && (
<AccordionItem value="12">
<AccordionHeader>

View File

@@ -1,300 +0,0 @@
import {
DefaultButton,
Dropdown,
IDropdownOption,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
IconButton,
Label,
Stack,
TextField,
} from "@fluentui/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import {
getDataTypeOptions,
getDistanceFunctionOptions,
getIndexTypeOptions,
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
import React, { FunctionComponent, useState } from "react";
export interface IAddVectorEmbeddingPolicyFormProps {
vectorEmbedding: VectorEmbedding[];
vectorIndex: VectorIndex[];
onVectorEmbeddingChange: (
vectorEmbeddings: VectorEmbedding[],
vectorIndexingPolicies: VectorIndex[],
validationPassed: boolean,
) => void;
}
export interface VectorEmbeddingPolicyData {
path: string;
dataType: VectorEmbedding["dataType"];
distanceFunction: VectorEmbedding["distanceFunction"];
dimensions: number;
indexType: VectorIndex["type"] | "none";
pathError: string;
dimensionsError: string;
}
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
fieldGroup: {
height: 27,
},
field: {
fontSize: 12,
padding: "0 8px",
},
};
const dropdownStyles = {
title: {
height: 27,
lineHeight: "24px",
fontSize: 12,
},
dropdown: {
height: 27,
lineHeight: "24px",
},
dropdownItem: {
fontSize: 12,
},
};
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
vectorEmbedding,
vectorIndex,
onVectorEmbeddingChange,
}): JSX.Element => {
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
let error = "";
if (!path) {
error = "Vector embedding path should not be empty";
}
if (
index >= 0 &&
vectorEmbeddingPolicyData?.find(
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
dataIndex !== index && vectorEmbedding.path === path,
)
) {
error = "Vector embedding path is already defined";
}
return error;
};
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
let error = "";
if (dimension <= 0 || dimension > 4096) {
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
}
if (indexType === "flat" && dimension > 505) {
error = "Maximum allowed dimension for flat index is 505";
}
return error;
};
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
const mergedData: VectorEmbeddingPolicyData[] = [];
vectorEmbedding.forEach((embedding) => {
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
mergedData.push({
...embedding,
indexType: matchingIndex?.type || "none",
pathError: onVectorEmbeddingPathError(embedding.path),
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
});
});
return mergedData;
};
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
initializeData(vectorEmbedding, vectorIndex),
);
React.useEffect(() => {
propagateData();
}, [vectorEmbeddingPolicyData]);
const propagateData = () => {
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
dataType: policy.dataType,
dimensions: policy.dimensions,
distanceFunction: policy.distanceFunction,
path: policy.path,
}));
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
.map(
(policy) =>
({
path: policy.path,
type: policy.indexType,
}) as VectorIndex,
);
const validationPassed = vectorEmbeddingPolicyData.every(
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
);
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
};
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
vectorEmbeddings[index].path = "/" + value;
} else {
vectorEmbeddings[index].path = value;
}
const error = onVectorEmbeddingPathError(value, index);
vectorEmbeddings[index].pathError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value.trim()) || 0;
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
const vectorEmbedding = vectorEmbeddings[index];
vectorEmbeddings[index].dimensions = value;
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
vectorEmbeddings[index].dimensionsError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
const vectorEmbedding = vectorEmbeddings[index];
vectorEmbeddings[index].indexType = option.key as never;
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
vectorEmbeddings[index].dimensionsError = error;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onVectorEmbeddingPolicyChange = (
index: number,
option: IDropdownOption,
property: VectorEmbeddingPolicyProperty,
): void => {
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
vectorEmbeddings[index][property] = option.key as never;
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onAdd = () => {
setVectorEmbeddingPolicyData([
...vectorEmbeddingPolicyData,
{
path: "",
dataType: "float32",
distanceFunction: "euclidean",
dimensions: 0,
indexType: "none",
pathError: onVectorEmbeddingPathError(""),
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
},
]);
};
const onDelete = (index: number) => {
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
return (
<Stack tokens={{ childrenGap: 4 }}>
{vectorEmbeddingPolicyData.length > 0 &&
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
<Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack
styles={{
root: {
margin: "0 0 6px 20px !important",
paddingLeft: 20,
width: "80%",
borderLeft: "1px solid",
},
}}
>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
<TextField
id={`vector-policy-path-${index + 1}`}
required={true}
placeholder="/vector1"
styles={textFieldStyles}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
value={vectorEmbeddingPolicy.path || ""}
errorMessage={vectorEmbeddingPolicy.pathError}
/>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getDataTypeOptions()}
selectedKey={vectorEmbeddingPolicy.dataType}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingPolicyChange(index, option, "dataType")
}
></Dropdown>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getDistanceFunctionOptions()}
selectedKey={vectorEmbeddingPolicy.distanceFunction}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
}
></Dropdown>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
<TextField
id={`vector-policy-dimension-${index + 1}`}
required={true}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.dimensions || 0)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onVectorEmbeddingDimensionsChange(index, event)
}
errorMessage={vectorEmbeddingPolicy.dimensionsError}
/>
</Stack>
<Stack>
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getIndexTypeOptions()}
selectedKey={vectorEmbeddingPolicy.indexType}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onVectorEmbeddingIndexTypeChange(index, option)
}
></Dropdown>
</Stack>
</Stack>
<IconButton
id={`delete-vector-policy-${index + 1}`}
iconProps={{ iconName: "Delete" }}
style={{ height: 27, margin: "auto" }}
onClick={() => onDelete(index)}
/>
</Stack>
</CollapsibleSectionComponent>
))}
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
Add vector embedding
</DefaultButton>
</Stack>
);
};

View File

@@ -309,40 +309,23 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
<Stack
className="panelGroupSpacing"
>
<Stack
horizontal={true}
<Text
className="panelTextBold"
variant="small"
>
<Text
className="panelTextBold"
variant="small"
>
Analytical store
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
<StyledLinkBase
href="https://aka.ms/analytical-store-overview"
target="_blank"
>
Learn more
</StyledLinkBase>
</Text>
<StyledTooltipHostBase
content={
<Text
variant="small"
>
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
<StyledLinkBase
href="https://aka.ms/analytical-store-overview"
target="_blank"
>
Learn more
</StyledLinkBase>
</Text>
}
directionalHint={4}
>
<Icon
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
</Text>
<Stack
horizontal={true}
verticalAlign="center"

View File

@@ -3,17 +3,11 @@
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
import {
AppStateComponentNames,
deleteState,
loadState,
saveState,
saveStateDebounced,
deleteSubComponentState,
readSubComponentState,
saveSubComponentState,
} from "Shared/AppStatePersistenceUtility";
import { userContext } from "UserContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
const componentName = AppStateComponentNames.DocumentsTab;
export enum SubComponentName {
ColumnSizes = "ColumnSizes",
@@ -21,6 +15,7 @@ export enum SubComponentName {
MainTabDivider = "MainTabDivider",
ColumnsSelection = "ColumnsSelection",
ColumnSort = "ColumnSort",
CurrentFilter = "CurrentFilter",
}
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
@@ -30,84 +25,22 @@ export type TabDivider = { leftPaneWidthPercent: number };
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
/**
*
* @param subComponentName
* @param collection
* @param defaultValue Will be returned if persisted state is not found
* @returns
*/
export const readSubComponentState = <T>(
// Wrap the ...SubComponentState functions for type safety
export const readDocumentsTabSubComponentState = <T>(
subComponentName: SubComponentName,
collection: ViewModels.CollectionBase,
defaultValue: T,
): T => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
return defaultValue;
}
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
const state = loadState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
}) as T;
return state || defaultValue;
};
/**
*
* @param subComponentName
* @param collection
* @param state State to save
* @param debounce true for high-frequency calls (e.g mouse drag events)
*/
export const saveSubComponentState = <T>(
export const saveDocumentsTabSubComponentState = <T>(
subComponentName: SubComponentName,
collection: ViewModels.CollectionBase,
state: T,
debounce?: boolean,
): void => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
return;
}
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
(debounce ? saveStateDebounced : saveState)(
{
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
},
state,
);
};
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
return;
}
deleteState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
});
};
export const deleteDocumentsTabSubComponentState = (
subComponentName: SubComponentName,
collection: ViewModels.CollectionBase,
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);

View File

@@ -49,6 +49,7 @@ jest.mock("Common/dataAccess/queryDocuments", () => ({
requestCharge: 1,
activityId: "activityId",
indexMetrics: "indexMetrics",
correlatedActivityId: undefined,
}),
})),
}));
@@ -385,22 +386,6 @@ describe("Documents tab (noSql API)", () => {
it("should render the page", () => {
expect(wrapper).toMatchSnapshot();
});
it("clicking on Edit filter should render the Apply Filter button", () => {
wrapper
.findWhere((node) => node.text() === "Edit Filter")
.at(0)
.simulate("click");
expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy();
});
it("clicking on Edit filter should render input for filter", () => {
wrapper
.findWhere((node) => node.text() === "Edit Filter")
.at(0)
.simulate("click");
expect(wrapper.find("Input.filterInput").exists()).toBeTruthy();
});
});
describe("Command bar buttons", () => {

View File

@@ -1,7 +1,6 @@
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import {
Button,
Input,
Link,
MessageBar,
MessageBarBody,
@@ -10,8 +9,7 @@ import {
makeStyles,
shorthands,
} from "@fluentui/react-components";
import { Dismiss16Filled } from "@fluentui/react-icons";
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import MongoUtility from "Common/MongoUtility";
import { createDocument } from "Common/dataAccess/createDocument";
@@ -23,9 +21,11 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { readDocument } from "Common/dataAccess/readDocument";
import { updateDocument } from "Common/dataAccess/updateDocument";
import { Platform, configContext } from "ConfigContext";
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -35,8 +35,9 @@ import {
FilterHistory,
SubComponentName,
TabDivider,
readSubComponentState,
saveSubComponentState,
deleteDocumentsTabSubComponentState,
readDocumentsTabSubComponentState,
saveDocumentsTabSubComponentState,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
@@ -74,6 +75,7 @@ const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we
const NO_SQL_THROTTLING_DOC_URL =
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer";
const loadMoreHeight = LayoutConstants.rowHeight;
export const useDocumentsTabStyles = makeStyles({
@@ -90,12 +92,6 @@ export const useDocumentsTabStyles = makeStyles({
alignItems: "center",
...cosmosShorthands.borderBottom(),
},
filterInput: {
flexGrow: 1,
},
appliedFilter: {
flexGrow: 1,
},
tableContainer: {
marginRight: tokens.spacingHorizontalXXXL,
},
@@ -146,6 +142,8 @@ export class DocumentsTabV2 extends TabsBase {
private title: string;
private resourceTokenPartitionKey: string;
protected persistedState: OpenCollectionTab;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
@@ -153,6 +151,13 @@ export class DocumentsTabV2 extends TabsBase {
this.title = options.title;
this.partitionKey = options.partitionKey;
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: options.isPreferredApiMongoDB ? TabKind.MongoDocuments : TabKind.SQLDocuments,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
};
}
public render(): JSX.Element {
@@ -556,8 +561,6 @@ export interface IDocumentsTabComponentProps {
isTabActive: boolean;
}
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
const getDefaultSqlFilters = (partitionKeys: string[]) =>
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
@@ -583,14 +586,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
onIsExecutingChange,
isTabActive,
}): JSX.Element => {
const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true);
const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false);
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
const [appliedFilter, setAppliedFilter] = useState<string>("");
const [filterContent, setFilterContent] = useState<string>("");
const [filterContent, setFilterContent] = useState<string>(() =>
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
);
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const filterInput = useRef<HTMLInputElement>(null);
const styles = useDocumentsTabStyles();
// Query
@@ -619,7 +620,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// State
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
leftPaneWidthPercent: 35,
}),
);
@@ -634,7 +635,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// User's filter history
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
);
// For progress bar for bulk delete (noSql)
@@ -657,12 +658,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
useEffect(() => {
if (isFilterFocused) {
filterInput.current?.focus();
}
}, [isFilterFocused]);
/**
* Recursively delete all documents by retrying throttled requests (429).
* This only works for NoSQL, because the bulk response includes status for each delete document request.
@@ -756,11 +751,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}, timeout);
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
const applyFilterButton = {
enabled: true,
visible: true,
};
const partitionKey: DataModels.PartitionKey = useMemo(
() => _partitionKey || (_collection && _collection.partitionKey),
[_collection, _partitionKey],
@@ -787,7 +777,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
};
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
SubComponentName.ColumnsSelection,
_collection,
undefined,
@@ -831,12 +821,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// This is executed in onActivate() in the original code.
useEffect(() => {
setKeyboardActions({
[KeyboardAction.SEARCH]: () => {
onShowFilterClick();
return true;
},
[KeyboardAction.CLEAR_SEARCH]: () => {
setFilterContent("");
updateFilterContent("");
refreshDocumentsGrid(true);
return true;
},
@@ -1317,12 +1303,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
],
);
const onShowFilterClick = () => {
setIsFilterCreated(true);
setIsFilterExpanded(true);
setIsFilterFocused(true);
};
const queryTimeoutEnabled = useCallback(
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
[isPreferredApiMongoDB],
@@ -1364,19 +1344,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedColumnIds,
]);
const onHideFilterClick = (): void => {
setIsFilterExpanded(false);
};
const onCloseButtonKeyDown: KeyboardEventHandler<HTMLSpanElement> = (event) => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
onHideFilterClick();
event.stopPropagation();
return false;
}
return true;
};
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
setDocumentIds(newDocumentsIds);
@@ -1518,14 +1485,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
};
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === "Enter") {
if (e.key === Constants.NormalizedEventKey.Enter) {
onApplyFilterClick();
// Suppress the default behavior of the key
e.preventDefault();
} else if (e.key === "Escape") {
onHideFilterClick();
// Suppress the default behavior of the key
e.preventDefault();
}
@@ -1697,7 +1659,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
SubComponentName.ColumnsSelection,
_collection,
undefined,
@@ -2008,7 +1970,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
setLastFilterContents(limitedLastFilterContents);
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
};
const refreshDocumentsGrid = useCallback(
@@ -2023,10 +1985,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
applyFilterButtonPressed,
});
// collapse filter
setAppliedFilter(filterContent);
setIsFilterExpanded(false);
// If apply filter is pressed, reset current selected document
if (applyFilterButtonPressed) {
setClickedRowIndex(RESET_INDEX);
@@ -2069,7 +2027,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
setSelectedColumnIds(newSelectedColumnIds);
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
selectedColumnIds: newSelectedColumnIds,
columnDefinitions,
});
@@ -2103,168 +2061,138 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
// -------------------------------------------------------
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
const options: InputDatalistDropdownOptionSection[] = [];
const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== "");
if (nonBlankLastFilters.length > 0) {
options.push({
label: "Saved filters",
options: nonBlankLastFilters,
});
}
options.push({
label: "Default filters",
options: isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
});
return options;
};
const updateFilterContent = (filter: string): void => {
if (filter === "" || filter === undefined) {
deleteDocumentsTabSubComponentState(SubComponentName.CurrentFilter, _collection);
} else {
saveDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, filter, true);
}
setFilterContent(filter);
};
return (
<CosmosFluentProvider className={styles.container}>
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
{isFilterCreated && (
<>
{!isFilterExpanded && !isPreferredApiMongoDB && (
<div className={styles.filterRow}>
<span>SELECT * FROM c</span>
<span className={styles.appliedFilter}>{appliedFilter}</span>
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
Edit Filter
</Button>
</div>
)}
{!isFilterExpanded && isPreferredApiMongoDB && (
<div className={styles.filterRow}>
{appliedFilter.length > 0 && <span>Filter :</span>}
{!(appliedFilter.length > 0) && <span className="noFilterApplied">No filter applied</span>}
<span className={styles.appliedFilter}>{appliedFilter}</span>
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
Edit Filter
</Button>
</div>
)}
{isFilterExpanded && (
<div className={styles.filterRow}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<Input
ref={filterInput}
type="text"
size="small"
list={`filtersList-${getUniqueId(_collection)}`}
className={`filterInput ${styles.filterInput}`}
title="Type a query predicate or choose one from the list."
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."
}
value={filterContent}
autoFocus={true}
onKeyDown={onFilterKeyDown}
onChange={(e) => setFilterContent(e.target.value)}
onBlur={() => setIsFilterFocused(false)}
/>
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
{addStringsNoDuplicate(
lastFilterContents,
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
).map((filter) => (
<option key={filter} value={filter} />
))}
</datalist>
<Button
appearance="primary"
size="small"
onClick={onApplyFilterClick}
disabled={!applyFilterButton.enabled}
aria-label="Apply filter"
tabIndex={0}
>
Apply Filter
</Button>
{!isPreferredApiMongoDB && isExecuting && (
<Button
appearance="primary"
size="small"
aria-label="Cancel Query"
onClick={() => queryAbortController.abort()}
tabIndex={0}
>
Cancel Query
</Button>
)}
<Button
aria-label="close filter"
tabIndex={0}
onClick={onHideFilterClick}
onKeyDown={onCloseButtonKeyDown}
appearance="transparent"
size="small"
icon={<Dismiss16Filled />}
/>
</div>
)}
</>
)}
{/* <Split> doesn't like to be a flex child */}
<div style={{ overflow: "hidden", height: "100%" }}>
<Allotment
onDragEnd={(sizes: number[]) => {
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
setTabStateData(tabStateData);
<div className={styles.filterRow}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<InputDataList
dropdownOptions={getFilterChoices()}
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."
}
title="Type a query predicate or choose one from the list."
value={filterContent}
onChange={updateFilterContent}
onKeyDown={onFilterKeyDown}
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
/>
<Button
appearance="primary"
size="small"
onClick={() => {
if (isExecuting) {
if (!isPreferredApiMongoDB) {
queryAbortController.abort();
}
} else {
onApplyFilterClick();
}
}}
disabled={isExecuting && isPreferredApiMongoDB}
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
tabIndex={0}
>
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
<div className={styles.tableContainer}>
<div
style={
{
height: "100%",
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
} /* Fix to make table not resize beyond parent's width */
}
>
<DocumentsTableComponent
onRefreshTable={() => refreshDocumentsGrid(false)}
items={tableItems}
onSelectedRowsChange={onSelectedRowsChange}
selectedRows={selectedRows}
size={tableContainerSizePx}
selectedColumnIds={selectedColumnIds}
columnDefinitions={columnDefinitions}
isRowSelectionDisabled={
isBulkDeleteDisabled ||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
}
onColumnSelectionChange={onColumnSelectionChange}
defaultColumnSelection={getInitialColumnSelection()}
collection={_collection}
isColumnSelectionDisabled={isPreferredApiMongoDB}
/>
</div>
</div>
{tableItems.length > 0 && (
<a
className={styles.loadMore}
role="button"
tabIndex={0}
onClick={() => loadNextPage(documentsIterator.iterator, false)}
onKeyDown={onLoadMoreKeyInput}
>
Load more
</a>
)}
</div>
</Allotment.Pane>
<Allotment.Pane minSize={30}>
<div style={{ height: "100%", width: "100%" }}>
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
<EditorReact
language={"json"}
content={selectedDocumentContent}
isReadOnly={false}
ariaLabel={"Document editor"}
lineNumbers={"on"}
theme={"_theme"}
onContentChanged={_onEditorContentChange}
enableWordWrapContextMenuItem={true}
/>
)}
{selectedRows.size > 1 && (
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
)}
</div>
</Allotment.Pane>
</Allotment>
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
</Button>
</div>
<Allotment
onDragEnd={(sizes: number[]) => {
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
setTabStateData(tabStateData);
}}
>
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
<div className={styles.tableContainer}>
<div
style={
{
height: "100%",
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
} /* Fix to make table not resize beyond parent's width */
}
>
<DocumentsTableComponent
onRefreshTable={() => refreshDocumentsGrid(false)}
items={tableItems}
onSelectedRowsChange={onSelectedRowsChange}
selectedRows={selectedRows}
size={tableContainerSizePx}
selectedColumnIds={selectedColumnIds}
columnDefinitions={columnDefinitions}
isRowSelectionDisabled={
isBulkDeleteDisabled ||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
}
onColumnSelectionChange={onColumnSelectionChange}
defaultColumnSelection={getInitialColumnSelection()}
collection={_collection}
isColumnSelectionDisabled={isPreferredApiMongoDB}
/>
</div>
</div>
{tableItems.length > 0 && (
<a
className={styles.loadMore}
role="button"
tabIndex={0}
onClick={() => loadNextPage(documentsIterator.iterator, false)}
onKeyDown={onLoadMoreKeyInput}
>
Load more
</a>
)}
</div>
</Allotment.Pane>
<Allotment.Pane minSize={30}>
<div style={{ height: "100%", width: "100%" }}>
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
<EditorReact
language={"json"}
content={selectedDocumentContent}
isReadOnly={false}
ariaLabel={"Document editor"}
lineNumbers={"on"}
theme={"_theme"}
onContentChanged={_onEditorContentChange}
enableWordWrapContextMenuItem={true}
/>
)}
{selectedRows.size > 1 && (
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
)}
</div>
</Allotment.Pane>
</Allotment>
</div>
{bulkDeleteOperation && (
<ProgressModalDialog

View File

@@ -67,6 +67,13 @@ jest.mock("Explorer/Controls/Dialog", () => ({
},
}));
// Added as recent change to @azure/core-util would cause randomUUID() to throw an error during jest tests.
// TODO: when not using beta version of @azure/cosmos sdk try removing this
jest.mock("@azure/core-util", () => ({
...jest.requireActual("@azure/core-util"),
randomUUID: jest.fn(),
}));
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
let newWrapper;
await act(async () => {

View File

@@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan
import {
ColumnSizesMap,
ColumnSort,
deleteSubComponentState,
readSubComponentState,
saveSubComponentState,
deleteDocumentsTabSubComponentState,
readDocumentsTabSubComponentState,
saveDocumentsTabSubComponentState,
SubComponentName,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
@@ -118,7 +118,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
const sortedRowsRef = React.useRef(null);
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
SubComponentName.ColumnSizes,
collection,
{},
);
const columnSizesPx: TableColumnSizingOptions = {};
selectedColumnIds.forEach((columnId) => {
if (
@@ -142,7 +146,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
sortDirection: "ascending" | "descending";
sortColumn: TableColumnId | undefined;
}>(() => {
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
if (!sort) {
return {
@@ -174,7 +178,12 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
return acc;
}, {} as ColumnSizesMap);
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
saveDocumentsTabSubComponentState<ColumnSizesMap>(
SubComponentName.ColumnSizes,
collection,
persistentSizes,
true,
);
return newSizingOptions;
});
@@ -186,11 +195,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
setColumnSort(event, columnId, direction);
if (columnId === undefined || direction === undefined) {
deleteSubComponentState(SubComponentName.ColumnSort, collection);
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
return;
}
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, {
columnId,
direction,
});
};
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes

View File

@@ -17,106 +17,124 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
>
<span>
SELECT * FROM c
SELECT * FROM c
</span>
<span
className="___r7kt3y0_0000000 fqerorx"
<InputDataList
bottomLink={
{
"text": "Learn more",
"url": "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer",
}
}
dropdownOptions={
[
{
"label": "Default filters",
"options": [
"WHERE c.id = "foo"",
"ORDER BY c._ts DESC",
"WHERE c.id = "foo" ORDER BY c._ts DESC",
"ORDER BY c._ts ASC",
"WHERE c.foo = "foo"",
],
},
]
}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="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."
title="Type a query predicate or choose one from the list."
value=""
/>
<Button
appearance="primary"
aria-label="Apply filter"
disabled={false}
onClick={[Function]}
size="small"
tabIndex={0}
>
Edit Filter
Apply Filter
</Button>
</div>
<div
style={
{
"height": "100%",
"overflow": "hidden",
}
}
<Allotment
onDragEnd={[Function]}
>
<Allotment
onDragEnd={[Function]}
<Allotment.Pane
minSize={55}
preferredSize="35%"
>
<Allotment.Pane
minSize={55}
preferredSize="35%"
<div
style={
{
"height": "100%",
"overflow": "hidden",
"width": "100%",
}
}
>
<div
style={
{
"height": "100%",
"overflow": "hidden",
"width": "100%",
}
}
className="___9o87uj0_0000000 ffefeo0"
>
<div
className="___9o87uj0_0000000 ffefeo0"
style={
{
"height": "100%",
"width": "calc(100% + -11px)",
}
}
>
<div
style={
<DocumentsTableComponent
collection={
{
"height": "100%",
"width": "calc(100% + -11px)",
"databaseId": "databaseId",
"id": [Function],
}
}
>
<DocumentsTableComponent
collection={
columnDefinitions={
[
{
"databaseId": "databaseId",
"id": [Function],
}
}
columnDefinitions={
[
{
"id": "id",
"isPartitionKey": false,
"label": "id",
},
]
}
defaultColumnSelection={
[
"id",
]
}
isColumnSelectionDisabled={false}
isRowSelectionDisabled={true}
items={[]}
onColumnSelectionChange={[Function]}
onRefreshTable={[Function]}
onSelectedRowsChange={[Function]}
selectedColumnIds={
[
"id",
]
}
selectedRows={Set {}}
/>
</div>
"id": "id",
"isPartitionKey": false,
"label": "id",
},
]
}
defaultColumnSelection={
[
"id",
]
}
isColumnSelectionDisabled={false}
isRowSelectionDisabled={true}
items={[]}
onColumnSelectionChange={[Function]}
onRefreshTable={[Function]}
onSelectedRowsChange={[Function]}
selectedColumnIds={
[
"id",
]
}
selectedRows={Set {}}
/>
</div>
</div>
</Allotment.Pane>
<Allotment.Pane
minSize={30}
>
<div
style={
{
"height": "100%",
"width": "100%",
}
</div>
</Allotment.Pane>
<Allotment.Pane
minSize={30}
>
<div
style={
{
"height": "100%",
"width": "100%",
}
/>
</Allotment.Pane>
</Allotment>
</div>
}
/>
</Allotment.Pane>
</Allotment>
</div>
</CosmosFluentProvider>
`;

View File

@@ -1,3 +1,4 @@
import { ActionType, TabKind } from "Contracts/ActionContracts";
import React from "react";
import MongoUtility from "../../../Common/MongoUtility";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -20,7 +21,7 @@ export class NewMongoQueryTab extends NewQueryTab {
private mongoQueryTabProps: IMongoQueryTabProps,
) {
super(options, mongoQueryTabProps);
this.queryText = "";
this.queryText = options.queryText ?? "";
this.iMongoQueryTabComponentProps = {
collection: options.collection,
isExecutionError: this.isExecutionError(),
@@ -28,6 +29,8 @@ export class NewMongoQueryTab extends NewQueryTab {
tabsBaseInstance: this,
queryText: this.queryText,
partitionKey: this.partitionKey,
splitterDirection: options.splitterDirection,
queryViewSizePercent: options.queryViewSizePercent,
container: this.mongoQueryTabProps.container,
onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance;
@@ -35,6 +38,26 @@ export class NewMongoQueryTab extends NewQueryTab {
isPreferredApiMongoDB: true,
monacoEditorSetting: "plaintext",
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
onUpdatePersistedState: (state: {
queryText: string;
splitterDirection: string;
queryViewSizePercent: number;
}): void => {
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: TabKind.SQLQuery,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
query: {
text: state.queryText,
},
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
queryViewSizePercent: state.queryViewSizePercent,
};
if (this.triggerPersistState) {
this.triggerPersistState();
}
},
};
}

View File

@@ -1,4 +1,5 @@
import { sendMessage } from "Common/MessageHandler";
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
import { MessageTypes } from "Contracts/MessageTypes";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { userContext } from "UserContext";
@@ -26,6 +27,8 @@ export class NewQueryTab extends TabsBase {
public iQueryTabComponentProps: IQueryTabComponentProps;
public iTabAccessor: ITabAccessor;
protected persistedState: OpenQueryTab;
constructor(
options: QueryTabOptions,
private props: IQueryTabProps,
@@ -39,12 +42,41 @@ export class NewQueryTab extends TabsBase {
tabsBaseInstance: this,
queryText: options.queryText,
partitionKey: this.partitionKey,
splitterDirection: options.splitterDirection,
queryViewSizePercent: options.queryViewSizePercent,
container: this.props.container,
onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance;
},
isPreferredApiMongoDB: false,
onUpdatePersistedState: (state: {
queryText: string;
splitterDirection: string;
queryViewSizePercent: number;
}): void => {
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: TabKind.SQLQuery,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
query: {
text: state.queryText,
},
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
queryViewSizePercent: state.queryViewSizePercent,
};
if (this.triggerPersistState) {
this.triggerPersistState();
}
},
};
// set initial state
this.iQueryTabComponentProps.onUpdatePersistedState({
queryText: options.queryText,
splitterDirection: options.splitterDirection,
queryViewSizePercent: options.queryViewSizePercent,
});
}
public render(): JSX.Element {

View File

@@ -34,6 +34,7 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
AppStateComponentNames: {
QueryCopilot: "QueryCopilot",
},
readSubComponentState: jest.fn(),
}));
describe("QueryTabComponent", () => {

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
import { FeedOptions } from "@azure/cosmos";
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
import { SplitterDirection } from "Common/Splitter";
import { Platform, configContext } from "ConfigContext";
@@ -18,13 +18,7 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import {
LocalStorageUtility,
StorageKey,
getDefaultQueryResultsView,
getRUThreshold,
ruThresholdEnabled,
} from "Shared/StorageUtility";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { Allotment } from "allotment";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
@@ -99,6 +93,13 @@ export interface IQueryTabComponentProps {
copilotEnabled?: boolean;
isSampleCopilotActive?: boolean;
copilotStore?: Partial<QueryCopilotState>;
splitterDirection?: "horizontal" | "vertical";
queryViewSizePercent?: number;
onUpdatePersistedState: (state: {
queryText: string;
splitterDirection: "vertical" | "horizontal";
queryViewSizePercent: number;
}) => void;
}
interface IQueryTabStates {
@@ -118,11 +119,13 @@ interface IQueryTabStates {
queryResultsView: SplitterDirection;
errors?: QueryError[];
modelMarkers?: monaco.editor.IMarkerData[];
queryViewSizePercent: number;
}
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
const styles = useQueryTabStyles();
const copilotStore = useCopilotStore();
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const queryTabProps = {
...props,
@@ -132,12 +135,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
isSampleCopilotActive: isSampleCopilotActive,
copilotStore: copilotStore,
};
return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />;
};
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
const styles = useQueryTabStyles();
return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />;
};
type QueryTabComponentImplProps = IQueryTabComponentProps & {
@@ -146,6 +149,8 @@ type QueryTabComponentImplProps = IQueryTabComponentProps & {
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
private static readonly DEBOUNCE_DELAY_MS = 1000;
public queryEditorId: string;
public executeQueryButton: Button;
public saveQueryButton: Button;
@@ -157,10 +162,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
private _iterator: MinimalQueryIterator;
private queryAbortController: AbortController;
queryEditor: React.RefObject<EditorReact>;
private timeoutId: NodeJS.Timeout | undefined;
constructor(props: QueryTabComponentImplProps) {
super(props);
this.queryEditor = createRef<EditorReact>();
this.state = {
@@ -176,7 +181,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
cancelQueryTimeoutID: undefined,
copilotActive: this._queryCopilotActive(),
currentTabActive: true,
queryResultsView: getDefaultQueryResultsView(),
queryResultsView:
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
queryViewSizePercent: props.queryViewSizePercent,
};
this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter";
@@ -207,6 +214,23 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
}
/**
* Helper function to save the query text in the query tab state
* Since it reads and writes to the same state, it is debounced
*/
private saveQueryTabStateDebounced = () => {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(async () => {
this.props.onUpdatePersistedState({
queryText: this.state.sqlQueryEditorContent,
splitterDirection: this.state.queryResultsView,
queryViewSizePercent: this.state.queryViewSizePercent,
});
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
};
private _queryCopilotActive(): boolean {
if (this.props.copilotEnabled) {
return readCopilotToggleStatus(userContext.databaseAccount);
@@ -344,19 +368,19 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
isExecutionError: false,
});
let queryOperationOptions: QueryOperationOptions;
if (userContext.apiType === "SQL" && ruThresholdEnabled()) {
const ruThreshold: number = getRUThreshold();
queryOperationOptions = {
ruCapPerOperation: ruThreshold,
} as QueryOperationOptions;
}
// let queryOperationOptions: QueryOperationOptions;
// if (userContext.apiType === "SQL" && ruThresholdEnabled()) {
// const ruThreshold: number = getRUThreshold();
// queryOperationOptions = {
// ruCapPerOperation: ruThreshold,
// } as QueryOperationOptions;
// }
const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage(
this.props.collection && this.props.collection.id(),
this._iterator,
firstItemIndex,
queryOperationOptions,
// queryOperationOptions,
);
this.props.tabsBaseInstance.isExecuting(true);
this.setState({
@@ -567,7 +591,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
};
}
private _setViewLayout(direction: SplitterDirection): void {
this.setState({ queryResultsView: direction });
this.setState({ queryResultsView: direction }, () => this.saveQueryTabStateDebounced());
// We'll need to refresh the context buttons to update the selected state of the view buttons
setTimeout(() => {
@@ -599,13 +623,16 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
this.setState({
sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "",
this.setState(
{
sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "",
// Clear the markers when the user edits the document.
modelMarkers: [],
});
// Clear the markers when the user edits the document.
modelMarkers: [],
},
() => this.saveQueryTabStateDebounced(),
);
if (this.isPreferredApiMongoDB) {
if (newContent.length > 0) {
this.executeQueryButton = {
@@ -704,8 +731,20 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
></QueryCopilotPromptbar>
)}
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
<Allotment key={vertical.toString()} vertical={vertical}>
<Allotment.Pane data-test="QueryTab/EditorPane">
<Allotment
key={vertical.toString()}
vertical={vertical}
onDragEnd={(sizes: number[]) => {
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
this.setState({ queryViewSizePercent }, () => this.saveQueryTabStateDebounced());
}}
>
<Allotment.Pane
data-test="QueryTab/EditorPane"
preferredSize={
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
}
>
<EditorReact
ref={this.queryEditor}
className={this.props.styles.queryEditor}

View File

@@ -1,3 +1,4 @@
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
import React from "react";
import * as ViewModels from "../../Contracts/ViewModels";
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
@@ -10,6 +11,18 @@ export class SettingsTabV2 extends TabsBase {
}
export class CollectionSettingsTabV2 extends SettingsTabV2 {
protected persistedState: OpenCollectionTab;
constructor(options: ViewModels.TabOptions) {
super(options);
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: TabKind.ScaleSettings,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
};
}
public onActivate(): void {
super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);

View File

@@ -1,9 +1,7 @@
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react";
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
import { sendMessage } from "Common/MessageHandler";
import { configContext } from "ConfigContext";
import { IpRule } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -35,7 +33,7 @@ interface TabsProps {
}
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
const [
showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
@@ -60,29 +58,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
return (
<div className="tabsManagerContainer">
{networkSettingsWarning && (
<MessageBar
messageBarType={MessageBarType.warning}
styles={defaultMessageBarStyles}
actions={
<MessageBarButton
onClick={() =>
sendMessage({
type:
userContext.apiType === "VCoreMongo"
? MessageTypes.OpenVCoreMongoNetworkingBlade
: MessageTypes.OpenPostgresNetworkingBlade,
})
}
>
Change network settings
</MessageBarButton>
}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
>
{networkSettingsWarning}
</MessageBar>
)}
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
<MessageBar
messageBarType={MessageBarType.warning}
@@ -343,7 +318,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if (
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
(userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length

View File

@@ -1,3 +1,4 @@
import { OpenTab } from "Contracts/ActionContracts";
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
@@ -30,6 +31,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
protected _theme: string;
public onLoadStartKey: number;
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
constructor(options: ViewModels.TabOptions) {
super();
this.index = options.index;
@@ -55,6 +58,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
};
}
// Called by useTabs to persist
public getPersistedState = (): OpenTab | null => this.persistedState;
public triggerPersistState: () => void = undefined;
public onCloseTabButtonClick(): void {
useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {

View File

@@ -52,6 +52,8 @@ export default class Collection implements ViewModels.Collection {
public partitionKeyProperties: string[];
public id: ko.Observable<string>;
public defaultTtl: ko.Observable<number>;
public vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
public fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>;
@@ -110,6 +112,8 @@ export default class Collection implements ViewModels.Collection {
this.id = ko.observable(data.id);
this.defaultTtl = ko.observable(data.defaultTtl);
this.vectorEmbeddingPolicy = ko.observable(data.vectorEmbeddingPolicy);
this.fullTextPolicy = ko.observable(data.fullTextPolicy);
this.indexingPolicy = ko.observable(data.indexingPolicy);
this.usageSizeInKB = ko.observable();
this.offer = ko.observable();
@@ -626,7 +630,13 @@ export default class Collection implements ViewModels.Collection {
}
};
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
public onNewQueryClick(
source: any,
event: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
) {
const collection: ViewModels.Collection = source.collection || source;
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id;
@@ -649,13 +659,21 @@ export default class Collection implements ViewModels.Collection {
queryText: queryText,
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
splitterDirection,
queryViewSizePercent,
},
{ container: this.container },
),
);
}
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
public onNewMongoQueryClick(
source: any,
event: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
) {
const collection: ViewModels.Collection = source.collection || source;
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
@@ -677,6 +695,9 @@ export default class Collection implements ViewModels.Collection {
node: this,
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
queryText,
splitterDirection,
queryViewSizePercent,
},
{
container: this.container,

View File

@@ -1,4 +1,4 @@
import { initializeIcons, Link, Text } from "@fluentui/react";
import { Link, Text } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css";
import * as React from "react";
import * as ReactDOM from "react-dom";
@@ -20,7 +20,7 @@ const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
const onInit = async () => {
const dataExplorerUrl = new URL("./", window.location.href).href;
initializeIcons();
// initializeIcons();
await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);

View File

@@ -1,4 +1,4 @@
import { initializeIcons } from "@fluentui/react";
// import { initializeIcons } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
import * as React from "react";
@@ -22,7 +22,7 @@ import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
initializeIcons();
// initializeIcons();
const App: React.FunctionComponent = () => {
// For handling encrypted portal tokens sent via query paramter

View File

@@ -2,7 +2,7 @@
import "./ReactDevTools";
// CSS Dependencies
import { initializeIcons, loadTheme } from "@fluentui/react";
import { loadTheme } from "@fluentui/react";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
@@ -62,7 +62,7 @@ import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
initializeIcons();
// initializeIcons();
const App: React.FunctionComponent = () => {
const isCarouselOpen = useCarousel((state) => state.shouldOpen);

View File

@@ -1,4 +1,4 @@
import { initializeIcons } from "@fluentui/react";
// import { initializeIcons } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css";
import React from "react";
import * as ReactDOM from "react-dom";
@@ -14,7 +14,7 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "../Utils/GalleryUtils";
const onInit = async () => {
initializeIcons();
// initializeIcons();
await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);

View File

@@ -38,6 +38,7 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
readonly restoreTabs: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@@ -108,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
restoreTabs: "true" === get("restoretabs"),
};
}

View File

@@ -1,4 +1,4 @@
import { initializeIcons, Spinner, SpinnerSize } from "@fluentui/react";
import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import ReactDOM from "react-dom";
import { withTranslation } from "react-i18next";
@@ -13,7 +13,7 @@ import "./SelfServe.less";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
initializeIcons();
// initializeIcons();
const loadTranslationFile = async (className: string): Promise<void> => {
const language = i18n.languages[0];

View File

@@ -266,7 +266,10 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
method: "GET",
apiVersion: "2023-05-01-preview",
queryParams: {
filter: "armRegionNameeq '" + regionShortName + "'",
filter:
"armRegionNameeq '" +
regionShortName +
"' and productDisplayName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
});

View File

@@ -1,12 +1,20 @@
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext";
import * as ViewModels from "../Contracts/ViewModels";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
// The component name whose state is being saved. Component name must not include special characters.
export enum AppStateComponentNames {
DocumentsTab = "DocumentsTab",
MostRecentActivity = "MostRecentActivity",
QueryCopilot = "QueryCopilot",
DataExplorerAction = "DataExplorerAction",
}
// Subcomponent for DataExplorerAction
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
export const PATH_SEPARATOR = "/"; // export for testing purposes
const SCHEMA_VERSION = 1;
@@ -72,12 +80,18 @@ export const hasState = (path: StorePath): boolean => {
};
// This is for high-frequency state changes
let timeoutId: NodeJS.Timeout | undefined;
// Keep track of timeouts per path
const pathToTimeoutIdMap = new Map<string, NodeJS.Timeout>();
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
const key = createKeyFromPath(path);
const timeoutId = pathToTimeoutIdMap.get(key);
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
pathToTimeoutIdMap.set(
key,
setTimeout(() => saveState(path, state), debounceDelayMs),
);
};
interface ApplicationState {
@@ -112,3 +126,93 @@ export const createKeyFromPath = (path: StorePath): string => {
export const deleteAllStates = (): void => {
LocalStorageUtility.removeEntry(StorageKey.AppState);
};
// Convenience functions
/**
*
* @param subComponentName
* @param collection
* @param defaultValue Will be returned if persisted state is not found
* @returns
*/
export const readSubComponentState = <T>(
componentName: AppStateComponentNames,
subComponentName: string,
collection: ViewModels.CollectionBase | undefined,
defaultValue: T,
): T => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
return defaultValue;
}
const state = loadState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection ? collection.databaseId : "",
containerName: collection ? collection.id() : "",
}) as T;
return state || defaultValue;
};
/**
*
* @param subComponentName
* @param collection
* @param state State to save
* @param debounce true for high-frequency calls (e.g mouse drag events)
*/
export const saveSubComponentState = <T>(
componentName: AppStateComponentNames,
subComponentName: string,
collection: ViewModels.CollectionBase | undefined,
state: T,
debounce?: boolean,
): void => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
return;
}
(debounce ? saveStateDebounced : saveState)(
{
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection ? collection.databaseId : "",
containerName: collection ? collection.id() : "",
},
state,
);
};
export const deleteSubComponentState = (
componentName: AppStateComponentNames,
subComponentName: string,
collection: ViewModels.CollectionBase,
) => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
return;
}
deleteState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
});
};

View File

@@ -104,7 +104,7 @@ export interface UserContext {
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
readonly dataPlaneRbacEnabled?: boolean;
readonly hasDataPlaneRbacSettingChanged?: boolean;
readonly refreshCosmosClient?: boolean;
}
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";

View File

@@ -20,3 +20,7 @@ export const isServerlessAccount = (): boolean => {
export const isVectorSearchEnabled = (): boolean => {
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
};
export const isFullTextSearchEnabled = (): boolean => {
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearch);
};

View File

@@ -93,7 +93,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
};
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,

View File

@@ -1,104 +0,0 @@
import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import { resetConfigContext, updateConfigContext } from "ConfigContext";
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
import { updateUserContext } from "UserContext";
import { MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
describe("NetworkUtility tests", () => {
describe("getNetworkSettingsWarningMessage", () => {
const publicAccessMessagePart = "Please enable public access to proceed";
const accessMessagePart = "Please allow access from Azure Portal to proceed";
let warningMessageResult: string;
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
beforeEach(() => {
warningMessageResult = undefined;
});
afterEach(() => {
resetConfigContext();
});
it("should return no message when publicNetworkAccess is enabled", async () => {
updateUserContext({
databaseAccount: {
properties: {
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
await getNetworkSettingsWarningMessage(warningMessageFunc);
expect(warningMessageResult).toBeUndefined();
});
it("should return publicAccessMessage when publicNetworkAccess is disabled", async () => {
updateUserContext({
databaseAccount: {
properties: {
publicNetworkAccess: "Disabled",
},
} as DatabaseAccount,
});
await getNetworkSettingsWarningMessage(warningMessageFunc);
expect(warningMessageResult).toContain(publicAccessMessagePart);
});
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => {
const portalBackendOutboundIPs: string[] = [
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac],
...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod],
];
updateUserContext({
databaseAccount: {
kind: "MongoDB",
properties: {
ipRules: portalBackendOutboundIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
});
let asyncWarningMessageResult: string;
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
expect(asyncWarningMessageResult).toBeUndefined();
});
it("should return accessMessage when incorrent ip rule is added to mongo/cassandra account per endpoint", async () => {
updateUserContext({
databaseAccount: {
kind: "MongoDB",
properties: {
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
});
let asyncWarningMessageResult: string;
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
expect(asyncWarningMessageResult).toContain(accessMessagePart);
});
// Postgres and vcore mongo account checks basically pass through to CheckFirewallRules so those
// tests are omitted here and included in CheckFirewallRules.test.ts
});
});

View File

@@ -1,99 +0,0 @@
import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
export const getNetworkSettingsWarningMessage = async (
setStateFunc: (warningMessage: string) => void,
): Promise<void> => {
const accountProperties = userContext.databaseAccount?.properties;
const accessMessage =
"The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
const publicAccessMessage =
"The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
if (userContext.apiType === "Postgres") {
checkFirewallRules(
"2022-11-08",
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
undefined,
setStateFunc,
accessMessage,
);
return;
} else if (userContext.apiType === "VCoreMongo") {
checkFirewallRules(
"2023-03-01-preview",
(rule) =>
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
undefined,
setStateFunc,
accessMessage,
);
return;
} else if (accountProperties) {
// public network access is disabled
if (
accountProperties.publicNetworkAccess !== "Enabled" &&
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
) {
setStateFunc(publicAccessMessage);
return;
}
const ipRules = accountProperties.ipRules;
// public network access is NOT set to "All networks"
if (ipRules?.length > 0) {
const isProdOrMpacPortalBackendEndpoint: boolean = [
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
].includes(configContext.PORTAL_BACKEND_ENDPOINT);
const portalBackendOutboundIPs: string[] = isProdOrMpacPortalBackendEndpoint
? [
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
]
: PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT];
let portalIPs: string[] = [...portalBackendOutboundIPs];
if (userContext.apiType === "Mongo") {
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
configContext.MONGO_PROXY_ENDPOINT,
);
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
portalIPs = [...portalIPs, ...mongoProxyOutboundIPs];
} else if (userContext.apiType === "Cassandra") {
const isProdOrMpacCassandraProxyEndpoint: boolean = [
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
? [
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
]
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
portalIPs = [...portalIPs, ...cassandraProxyOutboundIPs];
}
let numberOfMatches = 0;
ipRules.forEach((ipRule) => {
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
numberOfMatches++;
}
});
if (numberOfMatches !== portalIPs.length) {
setStateFunc(accessMessage);
}
}
}
};

View File

@@ -1237,6 +1237,7 @@ export interface SqlContainerResource {
id: string;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
/* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */
indexingPolicy?: IndexingPolicy;
@@ -1281,6 +1282,28 @@ export interface VectorEmbedding {
distanceFunction?: string;
}
export interface FullTextPolicy {
/**
* The default language for the full text .
*/
defaultLanguage: string;
/**
* The paths to be indexed for full text search.
*/
fullTextPaths: FullTextPath[];
}
export interface FullTextPath {
/**
* The path to be indexed for full text search.
*/
path: string;
/**
* The language for the full text path.
*/
language: string;
}
/* Cosmos DB indexing policy */
export interface IndexingPolicy {
/* Indicates if the indexing policy is automatic */
@@ -1301,6 +1324,8 @@ export interface IndexingPolicy {
spatialIndexes?: SpatialSpec[];
vectorIndexes?: VectorIndex[];
fullTextIndexes?: FullTextIndex[];
}
export interface VectorIndex {
@@ -1308,6 +1333,11 @@ export interface VectorIndex {
type?: string;
}
export interface FullTextIndex {
/** The path in the JSON document to index. */
path: string;
}
/* undocumented */
export interface ExcludedPath {
/* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */

View File

@@ -7,9 +7,13 @@ import Explorer from "Explorer/Explorer";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import {
AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME,
readSubComponentState,
} from "Shared/AppStatePersistenceUtility";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -80,6 +84,11 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
await updateContextForCopilot(explorer);
await updateContextForSampleData(explorer);
}
if (userContext.features.restoreTabs) {
restoreOpenTabs();
}
setExplorer(explorer);
}
};
@@ -132,7 +141,7 @@ async function configureFabric(): Promise<Explorer> {
await scheduleRefreshDatabaseResourceToken(true);
resolve(explorer);
await explorer.refreshAllDatabases();
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
if (userContext.fabricContext.isVisible) {
firstContainerOpened = true;
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
}
@@ -429,6 +438,7 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean
},
},
});
useTabs.getState().closeAllTabs();
const explorer = new Explorer();
return explorer;
}
@@ -731,8 +741,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
}
}
getNetworkSettingsWarningMessage(useTabs.getState().setNetworkSettingsWarning);
if (inputs.features) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
}
@@ -816,3 +824,17 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
interface SampledataconnectionResponse {
connectionString: string;
}
const restoreOpenTabs = () => {
const openTabsState = readSubComponentState<(DataExplorerAction | undefined)[]>(
AppStateComponentNames.DataExplorerAction,
OPEN_TABS_SUBCOMPONENT_NAME,
undefined,
[],
);
openTabsState.forEach((openTabState) => {
if (openTabState) {
handleOpenAction(openTabState, useDatabases.getState().databases, this);
}
});
};

View File

@@ -1,5 +1,11 @@
import { clamp } from "@fluentui/react";
import { OpenTab } from "Contracts/ActionContracts";
import { useSelectedNode } from "Explorer/useSelectedNode";
import {
AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME,
saveSubComponentState,
} from "Shared/AppStatePersistenceUtility";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels";
@@ -12,7 +18,6 @@ export interface TabsState {
openedReactTabs: ReactTabKind[];
activeTab: TabsBase | undefined;
activeReactTab: ReactTabKind | undefined;
networkSettingsWarning: string;
queryCopilotTabInitialInput: string;
isTabExecuting: boolean;
isQueryErrorThrown: boolean;
@@ -27,7 +32,6 @@ export interface TabsState {
closeAllNotebookTabs: (hardClose: boolean) => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void;
setNetworkSettingsWarning: (warningMessage: string) => void;
setQueryCopilotTabInitialInput: (input: string) => void;
setIsTabExecuting: (state: boolean) => void;
setIsQueryErrorThrown: (state: boolean) => void;
@@ -36,6 +40,8 @@ export interface TabsState {
selectLeftTab: () => void;
selectRightTab: () => void;
closeActiveTab: () => void;
closeAllTabs: () => void;
persistTabsState: () => void;
}
export enum ReactTabKind {
@@ -61,7 +67,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
activeTab: undefined,
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
networkSettingsWarning: "",
queryCopilotTabInitialInput: "",
isTabExecuting: false,
isQueryErrorThrown: false,
@@ -73,7 +78,9 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
},
activateNewTab: (tab: TabsBase): void => {
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
tab.triggerPersistState = get().persistTabsState;
tab.onActivate();
get().persistTabsState();
},
activateReactTab: (tabKind: ReactTabKind): void => {
// Clear the selected node when switching to a react tab.
@@ -130,6 +137,8 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
}
set({ openedTabs: updatedTabs });
get().persistTabsState();
},
closeAllNotebookTabs: (hardClose): void => {
const isNotebook = (tabKind: CollectionTabKind): boolean => {
@@ -178,7 +187,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
set({ openedReactTabs: updatedOpenedReactTabs });
},
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
setIsTabExecuting: (state: boolean) => {
set({ isTabExecuting: state });
@@ -226,4 +234,18 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
state.closeTab(state.activeTab);
}
},
closeAllTabs: () => {
set({ openedTabs: [], openedReactTabs: [], activeTab: undefined, activeReactTab: undefined });
},
persistTabsState: () => {
const state = get();
const openTabsStates = state.openedTabs.map((tab) => tab.getPersistedState());
saveSubComponentState<OpenTab[]>(
AppStateComponentNames.DataExplorerAction,
OPEN_TABS_SUBCOMPONENT_NAME,
undefined,
openTabsStates,
);
},
}));

View File

@@ -1,11 +1,11 @@
import { initializeIcons } from "@fluentui/react";
// import { initializeIcons } from "@fluentui/react";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import "jest-canvas-mock";
import enableHooks from "jest-react-hooks-shallow";
import { TextDecoder, TextEncoder } from "util";
configure({ adapter: new Adapter() });
initializeIcons();
// initializeIcons();
if (typeof window.URL.createObjectURL === "undefined") {
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });

View File

@@ -193,7 +193,12 @@ module.exports = function (_env = {}, argv = {}) {
];
if (argv.analyze) {
plugins.push(new BundleAnalyzerPlugin());
plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "bundle-report.html",
}),
);
}
return {
@@ -256,6 +261,17 @@ module.exports = function (_env = {}, argv = {}) {
},
}),
],
splitChunks: {
chunks: "all",
cacheGroups: {
fluentIcons: {
test: /[\\/]node_modules[\\/]@fluentui[\\/](font-icons-mdl2|react-icons)/,
name: "fluent-icons",
chunks: "all",
enforce: true,
},
},
},
},
watch: false,
// Hack since it is hard to disable watch entirely with webpack dev server https://github.com/webpack/webpack-dev-server/issues/1251#issuecomment-654240734