mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-09 20:49:12 +00:00
Compare commits
14 Commits
release/ho
...
sung_test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae92f2398 | ||
|
|
a0853c9167 | ||
|
|
2fdb3df4ae | ||
|
|
7c9802c07d | ||
|
|
e5609bd91e | ||
|
|
4b75e86b74 | ||
|
|
abf061089d | ||
|
|
ec25586a6e | ||
|
|
c15d1432b2 | ||
|
|
73d2686025 | ||
|
|
80b926214b | ||
|
|
070b7a4ca7 | ||
|
|
d61ff5dcb5 | ||
|
|
d42eebaa5a |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: cp -r ./Contracts ./dist/contracts
|
||||||
- run: cp -r ./configs ./dist/configs
|
- run: cp -r ./configs ./dist/configs
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -113,18 +113,18 @@ jobs:
|
|||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||||
steps:
|
steps:
|
||||||
- uses: nuget/setup-nuget@v1
|
- uses: nuget/setup-nuget@v2
|
||||||
with:
|
with:
|
||||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/prod.json config.json
|
- 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 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 pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: packages
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
@@ -137,11 +137,11 @@ jobs:
|
|||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||||
steps:
|
steps:
|
||||||
- uses: nuget/setup-nuget@v1
|
- uses: nuget/setup-nuget@v2
|
||||||
with:
|
with:
|
||||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/mpac.json config.json
|
- 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 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 pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: packages
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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,
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ a:focus {
|
|||||||
|
|
||||||
#divExplorer {
|
#divExplorer {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
padding: @FabricBoxMargin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
@@ -38,12 +37,12 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
background-color: #ffffff
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
@@ -68,7 +67,6 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: 2px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
@@ -122,7 +120,6 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -166,7 +163,6 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
@@ -175,8 +171,6 @@ a:focus {
|
|||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.filterbtnstyle {
|
.filterbtnstyle {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -202,12 +196,10 @@ a:focus {
|
|||||||
border: solid 1px #d1d1d1;
|
border: solid 1px #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gridRowSelected .tabdocumentsGridElement:hover {
|
.gridRowSelected .tabdocumentsGridElement:hover {
|
||||||
background-color: @FabricAccentLight !important;
|
background-color: @FabricAccentLight !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.refreshcol {
|
.refreshcol {
|
||||||
filter: brightness(0) saturate(100%);
|
filter: brightness(0) saturate(100%);
|
||||||
}
|
}
|
||||||
|
|||||||
251
package-lock.json
generated
251
package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@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/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.5.2",
|
"@azure/identity": "1.5.2",
|
||||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"@babel/preset-env": "7.24.7",
|
"@babel/preset-env": "7.24.7",
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.24.7",
|
||||||
"@babel/preset-typescript": "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",
|
"@testing-library/react": "11.2.3",
|
||||||
"@types/applicationinsights-js": "1.0.7",
|
"@types/applicationinsights-js": "1.0.7",
|
||||||
"@types/codemirror": "0.0.56",
|
"@types/codemirror": "0.0.56",
|
||||||
@@ -228,18 +228,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/abort-controller": {
|
"node_modules/@azure/abort-controller": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.2",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/abort-controller/node_modules/tslib": {
|
"node_modules/@azure/abort-controller/node_modules/tslib": {
|
||||||
"version": "2.6.2",
|
"version": "2.8.1",
|
||||||
"license": "0BSD"
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
"node_modules/@azure/arm-cosmosdb": {
|
"node_modules/@azure/arm-cosmosdb": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
@@ -251,15 +253,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-auth": {
|
"node_modules/@azure/core-auth": {
|
||||||
"version": "1.5.0",
|
"version": "1.9.0",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"@azure/core-util": "^1.1.0",
|
"@azure/core-util": "^1.11.0",
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-auth/node_modules/tslib": {
|
"node_modules/@azure/core-auth/node_modules/tslib": {
|
||||||
@@ -282,36 +285,61 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-client/node_modules/@azure/abort-controller": {
|
"node_modules/@azure/core-client/node_modules/tslib": {
|
||||||
"version": "2.1.2",
|
"version": "2.6.2",
|
||||||
"license": "MIT",
|
"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": {
|
"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"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-client/node_modules/tslib": {
|
"node_modules/@azure/core-rest-pipeline/node_modules/agent-base": {
|
||||||
"version": "2.6.2",
|
"version": "7.1.1",
|
||||||
"license": "0BSD"
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||||
},
|
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
||||||
"node_modules/@azure/core-rest-pipeline": {
|
|
||||||
"version": "1.12.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
"debug": "^4.3.4"
|
||||||
"@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"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"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": {
|
"node_modules/@azure/core-rest-pipeline/node_modules/tslib": {
|
||||||
@@ -319,13 +347,14 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-tracing": {
|
"node_modules/@azure/core-tracing": {
|
||||||
"version": "1.0.1",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-tracing/node_modules/tslib": {
|
"node_modules/@azure/core-tracing/node_modules/tslib": {
|
||||||
@@ -333,14 +362,15 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-util": {
|
"node_modules/@azure/core-util": {
|
||||||
"version": "1.6.1",
|
"version": "1.11.0",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-util/node_modules/tslib": {
|
"node_modules/@azure/core-util/node_modules/tslib": {
|
||||||
@@ -348,22 +378,20 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos": {
|
"node_modules/@azure/cosmos": {
|
||||||
"version": "4.0.1-beta.3",
|
"version": "4.2.0-beta.1",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz",
|
||||||
|
"integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"@azure/core-auth": "^1.3.0",
|
"@azure/core-auth": "^1.7.1",
|
||||||
"@azure/core-rest-pipeline": "^1.2.0",
|
"@azure/core-rest-pipeline": "^1.15.1",
|
||||||
"@azure/core-tracing": "^1.0.0",
|
"@azure/core-tracing": "^1.1.1",
|
||||||
"debug": "^4.1.1",
|
"@azure/core-util": "^1.8.1",
|
||||||
"fast-json-stable-stringify": "^2.1.0",
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
"jsbi": "^3.1.3",
|
"jsbi": "^4.3.0",
|
||||||
"node-abort-controller": "^3.0.0",
|
|
||||||
"priorityqueuejs": "^2.0.0",
|
"priorityqueuejs": "^2.0.0",
|
||||||
"semaphore": "^1.0.5",
|
"semaphore": "^1.1.0",
|
||||||
"tslib": "^2.2.0",
|
"tslib": "^2.6.2"
|
||||||
"universal-user-agent": "^6.0.0",
|
|
||||||
"uuid": "^8.3.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@@ -10147,34 +10175,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"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,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.44.0"
|
"playwright": "1.49.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
}
|
|
||||||
},
|
|
||||||
"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_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
@@ -11708,6 +11720,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
@@ -14775,6 +14788,15 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/bintrees": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -19437,6 +19459,12 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/filesize": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -19895,6 +19923,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -20024,6 +20053,19 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21095,6 +21137,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tootallnate/once": "2",
|
"@tootallnate/once": "2",
|
||||||
@@ -24129,6 +24172,24 @@
|
|||||||
"fsevents": "^1.2.7"
|
"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": {
|
"node_modules/jest-haste-map/node_modules/jest-worker": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -27067,8 +27128,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsbi": {
|
"node_modules/jsbi": {
|
||||||
"version": "3.2.5",
|
"version": "4.3.0",
|
||||||
"license": "Apache-2.0"
|
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
|
||||||
},
|
},
|
||||||
"node_modules/jsbn": {
|
"node_modules/jsbn": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
@@ -29737,7 +29799,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/node-abort-controller": {
|
"node_modules/node-abort-controller": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"license": "MIT"
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
@@ -30764,15 +30828,34 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright": {
|
||||||
"version": "1.44.0",
|
"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,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/plotly.js-cartesian-dist-min": {
|
"node_modules/plotly.js-cartesian-dist-min": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@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/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.5.2",
|
"@azure/identity": "1.5.2",
|
||||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
"@babel/preset-env": "7.24.7",
|
"@babel/preset-env": "7.24.7",
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.24.7",
|
||||||
"@babel/preset-typescript": "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",
|
"@testing-library/react": "11.2.3",
|
||||||
"@types/applicationinsights-js": "1.0.7",
|
"@types/applicationinsights-js": "1.0.7",
|
||||||
"@types/codemirror": "0.0.56",
|
"@types/codemirror": "0.0.56",
|
||||||
@@ -170,10 +170,10 @@
|
|||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-canvas-mock": "2.5.2",
|
"jest-canvas-mock": "2.5.2",
|
||||||
"jest-circus": "29.7.0",
|
"jest-circus": "29.7.0",
|
||||||
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"jest-html-loader": "1.0.0",
|
"jest-html-loader": "1.0.0",
|
||||||
"jest-react-hooks-shallow": "1.5.1",
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "3.0.2",
|
"jest-trx-results-processor": "3.0.2",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "11.1.3",
|
"less-loader": "11.1.3",
|
||||||
"less-vars-loader": "1.1.0",
|
"less-vars-loader": "1.1.0",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export class CapabilityNames {
|
|||||||
public static readonly EnableMongo: string = "EnableMongo";
|
public static readonly EnableMongo: string = "EnableMongo";
|
||||||
public static readonly EnableServerless: string = "EnableServerless";
|
public static readonly EnableServerless: string = "EnableServerless";
|
||||||
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
||||||
|
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CapacityMode {
|
export enum CapacityMode {
|
||||||
@@ -148,7 +149,7 @@ export class PortalBackendEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MongoProxyEndpoints {
|
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 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 Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { AuthType } from "../AuthType";
|
|||||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -189,10 +189,19 @@ let _client: Cosmos.CosmosClient;
|
|||||||
|
|
||||||
export function client(): Cosmos.CosmosClient {
|
export function client(): Cosmos.CosmosClient {
|
||||||
if (_client) {
|
if (_client) {
|
||||||
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
if (!userContext.refreshCosmosClient) {
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
|
_client.dispose();
|
||||||
|
_client = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.refreshCosmosClient) {
|
||||||
|
updateUserContext({
|
||||||
|
refreshCosmosClient: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
// import { QueryOperationOptions } from "@azure/cosmos";
|
||||||
import { QueryResults } from "../Contracts/ViewModels";
|
import { QueryResults } from "../Contracts/ViewModels";
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
@@ -11,17 +11,13 @@ interface QueryResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MinimalQueryIterator {
|
export interface MinimalQueryIterator {
|
||||||
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
fetchNext: () => Promise<QueryResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick<QueryIterator<any>, "fetchNext">;
|
// Pick<QueryIterator<any>, "fetchNext">;
|
||||||
|
|
||||||
export function nextPage(
|
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
||||||
documentsIterator: MinimalQueryIterator,
|
return documentsIterator.fetchNext().then((response) => {
|
||||||
firstItemIndex: number,
|
|
||||||
queryOperationOptions?: QueryOperationOptions,
|
|
||||||
): Promise<QueryResults> {
|
|
||||||
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
|
||||||
const documents = response.resources;
|
const documents = response.resources;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
||||||
|
|||||||
@@ -722,63 +722,63 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||||
[MongoProxyApi.ResourceList]: [
|
[MongoProxyApi.ResourceList]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.QueryDocuments]: [
|
[MongoProxyApi.QueryDocuments]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.CreateDocument]: [
|
[MongoProxyApi.CreateDocument]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.ReadDocument]: [
|
[MongoProxyApi.ReadDocument]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.UpdateDocument]: [
|
[MongoProxyApi.UpdateDocument]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.DeleteDocument]: [
|
[MongoProxyApi.DeleteDocument]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.CreateCollectionWithProxy]: [
|
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.LegacyMongoShell]: [
|
[MongoProxyApi.LegacyMongoShell]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[MongoProxyApi.BulkDelete]: [
|
[MongoProxyApi.BulkDelete]: [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
|||||||
if (params.vectorEmbeddingPolicy) {
|
if (params.vectorEmbeddingPolicy) {
|
||||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||||
}
|
}
|
||||||
|
if (params.fullTextPolicy) {
|
||||||
|
resource.fullTextPolicy = params.fullTextPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -270,6 +273,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
|||||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||||
analyticalStorageTtl: params.analyticalStorageTtl,
|
analyticalStorageTtl: params.analyticalStorageTtl,
|
||||||
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
||||||
|
fullTextPolicy: params.fullTextPolicy,
|
||||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||||
const collectionOptions: RequestOptions = {};
|
const collectionOptions: RequestOptions = {};
|
||||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
// import { QueryOperationOptions } from "@azure/cosmos";
|
||||||
import { QueryResults } from "../../Contracts/ViewModels";
|
import { QueryResults } from "../../Contracts/ViewModels";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { getEntityName } from "../DocumentUtility";
|
import { getEntityName } from "../DocumentUtility";
|
||||||
@@ -9,13 +9,13 @@ export const queryDocumentsPage = async (
|
|||||||
resourceName: string,
|
resourceName: string,
|
||||||
documentsIterator: MinimalQueryIterator,
|
documentsIterator: MinimalQueryIterator,
|
||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
queryOperationOptions?: QueryOperationOptions,
|
// queryOperationOptions?: QueryOperationOptions,
|
||||||
): Promise<QueryResults> => {
|
): Promise<QueryResults> => {
|
||||||
const entityName = getEntityName();
|
const entityName = getEntityName();
|
||||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
|
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||||
const itemCount = (result.documents && result.documents.length) || 0;
|
const itemCount = (result.documents && result.documents.length) || 0;
|
||||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export enum TabKind {
|
|||||||
Graph,
|
Graph,
|
||||||
SQLQuery,
|
SQLQuery,
|
||||||
ScaleSettings,
|
ScaleSettings,
|
||||||
|
MongoQuery,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +52,8 @@ export interface OpenCollectionTab extends OpenTab {
|
|||||||
*/
|
*/
|
||||||
export interface OpenQueryTab extends OpenCollectionTab {
|
export interface OpenQueryTab extends OpenCollectionTab {
|
||||||
query: QueryInfo;
|
query: QueryInfo;
|
||||||
|
splitterDirection?: "vertical" | "horizontal";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export interface Collection extends Resource {
|
|||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
|
fullTextPolicy?: FullTextPolicy;
|
||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
computedProperties?: ComputedProperties;
|
computedProperties?: ComputedProperties;
|
||||||
@@ -199,11 +200,19 @@ export interface IndexingPolicy {
|
|||||||
compositeIndexes?: any[];
|
compositeIndexes?: any[];
|
||||||
spatialIndexes?: any[];
|
spatialIndexes?: any[];
|
||||||
vectorIndexes?: VectorIndex[];
|
vectorIndexes?: VectorIndex[];
|
||||||
|
fullTextIndexes?: FullTextIndex[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorIndex {
|
export interface VectorIndex {
|
||||||
path: string;
|
path: string;
|
||||||
type: "flat" | "diskANN" | "quantizedFlat";
|
type: "flat" | "diskANN" | "quantizedFlat";
|
||||||
|
diskANNShardKey?: string;
|
||||||
|
indexingSearchListSize?: number;
|
||||||
|
quantizationByteSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullTextIndex {
|
||||||
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputedProperty {
|
export interface ComputedProperty {
|
||||||
@@ -342,6 +351,7 @@ export interface CreateCollectionParams {
|
|||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
createMongoWildcardIndex?: boolean;
|
createMongoWildcardIndex?: boolean;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
|
fullTextPolicy?: FullTextPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicy {
|
export interface VectorEmbeddingPolicy {
|
||||||
@@ -355,6 +365,16 @@ export interface VectorEmbedding {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FullTextPolicy {
|
||||||
|
defaultLanguage: string;
|
||||||
|
fullTextPaths: FullTextPath[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullTextPath {
|
||||||
|
path: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReadDatabaseOfferParams {
|
export interface ReadDatabaseOfferParams {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseResourceId?: string;
|
databaseResourceId?: string;
|
||||||
|
|||||||
@@ -115,7 +115,13 @@ export interface CollectionBase extends TreeNode {
|
|||||||
isSampleCollection?: boolean;
|
isSampleCollection?: boolean;
|
||||||
|
|
||||||
onDocumentDBDocumentsClick(): void;
|
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;
|
expandCollection(): void;
|
||||||
collapseCollection(): void;
|
collapseCollection(): void;
|
||||||
getDatabase(): Database;
|
getDatabase(): Database;
|
||||||
@@ -126,6 +132,8 @@ export interface Collection extends CollectionBase {
|
|||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
schema?: DataModels.ISchema;
|
schema?: DataModels.ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
|
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
||||||
|
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
usageSizeInKB: ko.Observable<number>;
|
usageSizeInKB: ko.Observable<number>;
|
||||||
@@ -149,7 +157,13 @@ export interface Collection extends CollectionBase {
|
|||||||
onSettingsClick: () => Promise<void>;
|
onSettingsClick: () => Promise<void>;
|
||||||
|
|
||||||
onNewGraphClick(): 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;
|
onNewMongoShellClick(): void;
|
||||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||||
@@ -309,6 +323,8 @@ export interface QueryTabOptions extends TabOptions {
|
|||||||
partitionKey?: DataModels.PartitionKey;
|
partitionKey?: DataModels.PartitionKey;
|
||||||
queryText?: string;
|
queryText?: string;
|
||||||
resourceTokenPartitionKey?: string;
|
resourceTokenPartitionKey?: string;
|
||||||
|
splitterDirection?: "horizontal" | "vertical";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptTabOption extends TabOptions {
|
export interface ScriptTabOption extends TabOptions {
|
||||||
|
|||||||
@@ -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 * as React from "react";
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
@@ -9,6 +9,9 @@ export interface CollapsibleSectionProps {
|
|||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||||
|
showDelete?: boolean;
|
||||||
|
onDelete?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
@@ -69,6 +72,20 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</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>
|
</Stack>
|
||||||
{this.state.isExpanded && this.props.children}
|
{this.state.isExpanded && this.props.children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
describe("AddFullTextPolicyForm", () => {
|
||||||
|
//CTODO: add tests
|
||||||
|
it.skip("should render correctly", () => {});
|
||||||
|
});
|
||||||
@@ -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)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,11 +4,11 @@ import {
|
|||||||
ComputedPropertiesComponentProps,
|
ComputedPropertiesComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||||
import {
|
import {
|
||||||
ContainerVectorPolicyComponent,
|
ContainerPolicyComponent,
|
||||||
ContainerVectorPolicyComponentProps,
|
ContainerPolicyComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
@@ -105,6 +105,13 @@ export interface SettingsComponentState {
|
|||||||
isSubSettingsSaveable: boolean;
|
isSubSettingsSaveable: boolean;
|
||||||
isSubSettingsDiscardable: boolean;
|
isSubSettingsDiscardable: boolean;
|
||||||
|
|
||||||
|
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||||
|
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||||
|
fullTextPolicy: DataModels.FullTextPolicy;
|
||||||
|
fullTextPolicyBaseline: DataModels.FullTextPolicy;
|
||||||
|
shouldDiscardContainerPolicies: boolean;
|
||||||
|
isContainerPolicyDirty: boolean;
|
||||||
|
|
||||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||||
shouldDiscardIndexingPolicy: boolean;
|
shouldDiscardIndexingPolicy: boolean;
|
||||||
@@ -149,6 +156,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
@@ -164,6 +172,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
|
||||||
@@ -203,6 +212,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
|
|
||||||
|
vectorEmbeddingPolicy: undefined,
|
||||||
|
vectorEmbeddingPolicyBaseline: undefined,
|
||||||
|
fullTextPolicy: undefined,
|
||||||
|
fullTextPolicyBaseline: undefined,
|
||||||
|
shouldDiscardContainerPolicies: false,
|
||||||
|
isContainerPolicyDirty: false,
|
||||||
|
|
||||||
indexingPolicyContent: undefined,
|
indexingPolicyContent: undefined,
|
||||||
indexingPolicyContentBaseline: undefined,
|
indexingPolicyContentBaseline: undefined,
|
||||||
shouldDiscardIndexingPolicy: false,
|
shouldDiscardIndexingPolicy: false,
|
||||||
@@ -307,6 +323,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return (
|
return (
|
||||||
this.state.isScaleSaveable ||
|
this.state.isScaleSaveable ||
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
|
this.state.isContainerPolicyDirty ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
this.state.isComputedPropertiesDirty ||
|
||||||
@@ -318,6 +335,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return (
|
return (
|
||||||
this.state.isScaleDiscardable ||
|
this.state.isScaleDiscardable ||
|
||||||
this.state.isSubSettingsDiscardable ||
|
this.state.isSubSettingsDiscardable ||
|
||||||
|
this.state.isContainerPolicyDirty ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
this.state.isComputedPropertiesDirty ||
|
||||||
@@ -405,6 +423,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||||
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
||||||
|
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicyBaseline,
|
||||||
|
fullTextPolicy: this.state.fullTextPolicyBaseline,
|
||||||
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
||||||
indexesToAdd: [],
|
indexesToAdd: [],
|
||||||
indexesToDrop: [],
|
indexesToDrop: [],
|
||||||
@@ -416,11 +436,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
||||||
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
||||||
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
||||||
|
shouldDiscardContainerPolicies: true,
|
||||||
shouldDiscardIndexingPolicy: true,
|
shouldDiscardIndexingPolicy: true,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
|
isContainerPolicyDirty: false,
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isMongoIndexingPolicySaveable: false,
|
isMongoIndexingPolicySaveable: false,
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
@@ -448,9 +470,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
||||||
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
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 =>
|
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||||
|
|
||||||
|
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
||||||
|
|
||||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||||
|
|
||||||
private logIndexingPolicySuccessMessage = (): void => {
|
private logIndexingPolicySuccessMessage = (): void => {
|
||||||
@@ -538,6 +568,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
||||||
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
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 =>
|
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
||||||
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
||||||
|
|
||||||
@@ -691,6 +727,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: 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 indexingPolicyContent = this.collection.indexingPolicy();
|
||||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||||
@@ -724,6 +764,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||||
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
||||||
|
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
|
||||||
|
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
|
||||||
|
fullTextPolicy: fullTextPolicy,
|
||||||
|
fullTextPolicyBaseline: fullTextPolicy,
|
||||||
indexingPolicyContent: indexingPolicyContent,
|
indexingPolicyContent: indexingPolicyContent,
|
||||||
indexingPolicyContentBaseline: indexingPolicyContent,
|
indexingPolicyContentBaseline: indexingPolicyContent,
|
||||||
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
||||||
@@ -854,6 +898,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
|
this.state.isContainerPolicyDirty ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty
|
this.state.isComputedPropertiesDirty
|
||||||
@@ -875,6 +920,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||||
newCollection.defaultTtl = defaultTtl;
|
newCollection.defaultTtl = defaultTtl;
|
||||||
|
|
||||||
|
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
|
||||||
|
|
||||||
|
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
||||||
|
|
||||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||||
|
|
||||||
newCollection.changeFeedPolicy =
|
newCollection.changeFeedPolicy =
|
||||||
@@ -913,6 +962,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||||
|
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
|
||||||
|
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
|
||||||
|
|
||||||
if (wasIndexingPolicyModified) {
|
if (wasIndexingPolicyModified) {
|
||||||
await this.refreshIndexTransformationProgress();
|
await this.refreshIndexTransformationProgress();
|
||||||
@@ -921,6 +972,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({
|
this.setState({
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
|
isContainerPolicyDirty: false,
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
isComputedPropertiesDirty: false,
|
isComputedPropertiesDirty: false,
|
||||||
@@ -1091,6 +1143,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
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 = {
|
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||||
@@ -1148,10 +1215,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
|
||||||
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1165,10 +1228,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isVectorSearchEnabled) {
|
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||||
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
describe("ContainerPolicyComponent", () => {
|
||||||
|
//CTODO: add tests
|
||||||
|
it.skip("should render correctly", () => {});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -120,11 +120,6 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
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) && (
|
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
export type isDirtyTypes =
|
||||||
|
| boolean
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| DataModels.IndexingPolicy
|
||||||
|
| DataModels.ComputedProperties
|
||||||
|
| DataModels.VectorEmbedding[]
|
||||||
|
| DataModels.FullTextPolicy;
|
||||||
export const TtlOff = "off";
|
export const TtlOff = "off";
|
||||||
export const TtlOn = "on";
|
export const TtlOn = "on";
|
||||||
export const TtlOnNoDefault = "on-nodefault";
|
export const TtlOnNoDefault = "on-nodefault";
|
||||||
@@ -50,6 +57,11 @@ export enum SettingsV2TabTypes {
|
|||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ContainerPolicyTabTypes {
|
||||||
|
VectorPolicyTab,
|
||||||
|
FullTextPolicyTab,
|
||||||
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
isSaveable: boolean;
|
isSaveable: boolean;
|
||||||
isDiscardable: boolean;
|
isDiscardable: boolean;
|
||||||
@@ -154,7 +166,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||||
return "Computed Properties";
|
return "Computed Properties";
|
||||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||||
return "Container Vector Policy (preview)";
|
return "Container Policies";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export const collection = {
|
|||||||
query: "query",
|
query: "query",
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
||||||
|
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
|
"fullTextPolicy": [Function],
|
||||||
"geospatialConfig": [Function],
|
"geospatialConfig": [Function],
|
||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
@@ -71,6 +72,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
@@ -132,6 +134,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
|
"fullTextPolicy": [Function],
|
||||||
"geospatialConfig": [Function],
|
"geospatialConfig": [Function],
|
||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
@@ -148,6 +151,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
displayedTtlSeconds="5"
|
displayedTtlSeconds="5"
|
||||||
@@ -249,6 +253,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
|
"fullTextPolicy": [Function],
|
||||||
"geospatialConfig": [Function],
|
"geospatialConfig": [Function],
|
||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
@@ -265,6 +270,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
|
|||||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
|
||||||
|
|
||||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||||
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
component = render(
|
component = render(
|
||||||
<AddVectorEmbeddingPolicyForm
|
<VectorEmbeddingPoliciesComponent
|
||||||
vectorEmbedding={mockVectorEmbedding}
|
vectorEmbeddings={mockVectorEmbedding}
|
||||||
vectorIndex={mockVectorIndex}
|
vectorIndexes={mockVectorIndex}
|
||||||
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("calls onDelete when delete button is clicked", async () => {
|
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);
|
fireEvent.click(deleteButton);
|
||||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||||
@@ -49,21 +49,19 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
|
|
||||||
test("validates input correctly", async () => {
|
test("validates input correctly", async () => {
|
||||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
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,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() =>
|
() =>
|
||||||
expect(
|
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(),
|
||||||
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
{
|
{
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
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,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
await waitFor(
|
await waitFor(
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1134,7 +1134,7 @@ export default class Explorer {
|
|||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? 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();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||||
@@ -56,6 +57,19 @@ function openCollectionTab(
|
|||||||
continue;
|
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
|
//expand database first if not expanded to load the collections
|
||||||
if (!database.isDatabaseExpanded?.()) {
|
if (!database.isDatabaseExpanded?.()) {
|
||||||
database.expandDatabase?.();
|
database.expandDatabase?.();
|
||||||
@@ -121,10 +135,28 @@ function openCollectionTab(
|
|||||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||||
) {
|
) {
|
||||||
|
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||||
collection.onNewQueryClick(
|
collection.onNewQueryClick(
|
||||||
collection,
|
collection,
|
||||||
undefined,
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
|||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { 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 { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -30,7 +34,12 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import {
|
||||||
|
isCapabilityEnabled,
|
||||||
|
isFullTextSearchEnabled,
|
||||||
|
isServerlessAccount,
|
||||||
|
isVectorSearchEnabled,
|
||||||
|
} from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||||
@@ -109,6 +118,9 @@ export interface AddCollectionPanelState {
|
|||||||
vectorIndexingPolicy: DataModels.VectorIndex[];
|
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||||
vectorPolicyValidated: boolean;
|
vectorPolicyValidated: boolean;
|
||||||
|
fullTextPolicy: DataModels.FullTextPolicy;
|
||||||
|
fullTextIndexes: DataModels.FullTextIndex[];
|
||||||
|
fullTextPolicyValidated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@@ -147,6 +159,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
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() && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Stack horizontal>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
Analytical store
|
{this.getAnalyticalStorageContent()}
|
||||||
</Text>
|
</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>
|
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<div role="radiogroup">
|
<div role="radiogroup">
|
||||||
@@ -890,9 +892,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
<AddVectorEmbeddingPolicyForm
|
<VectorEmbeddingPoliciesComponent
|
||||||
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
vectorEmbeddings={this.state.vectorEmbeddingPolicy}
|
||||||
vectorIndex={this.state.vectorIndexingPolicy}
|
vectorIndexes={this.state.vectorIndexingPolicy}
|
||||||
onVectorEmbeddingChange={(
|
onVectorEmbeddingChange={(
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||||
vectorIndexingPolicy: DataModels.VectorIndex[],
|
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||||
@@ -906,6 +908,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</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" && (
|
{userContext.apiType !== "Tables" && (
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
@@ -1187,7 +1217,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
private getAnalyticalStorageContent(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
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 {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1274,6 +1317,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldShowFullTextSearchParameters() {
|
||||||
|
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
|
}
|
||||||
|
|
||||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||||
if (this.state.uniqueKeys?.length === 0) {
|
if (this.state.uniqueKeys?.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -1330,11 +1377,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
|
if (!this.state.vectorPolicyValidated) {
|
||||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.fullTextPolicyValidated) {
|
||||||
|
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1423,6 +1477,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldShowFullTextSearchParameters()) {
|
||||||
|
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
|
||||||
|
}
|
||||||
|
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
database: {
|
database: {
|
||||||
id: databaseId,
|
id: databaseId,
|
||||||
@@ -1482,6 +1540,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
|
fullTextPolicy: this.state.fullTextPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({ isExecuting: true });
|
this.setState({ isExecuting: true });
|
||||||
|
|||||||
@@ -193,6 +193,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
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) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||||
if (
|
if (
|
||||||
@@ -202,7 +213,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
) {
|
) {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: true,
|
dataPlaneRbacEnabled: true,
|
||||||
hasDataPlaneRbacSettingChanged: true,
|
|
||||||
});
|
});
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
try {
|
try {
|
||||||
@@ -226,7 +236,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
} else {
|
} else {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: false,
|
dataPlaneRbacEnabled: false,
|
||||||
hasDataPlaneRbacSettingChanged: true,
|
|
||||||
});
|
});
|
||||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
||||||
@@ -564,7 +573,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<>
|
<>
|
||||||
<AccordionItem value="3">
|
<AccordionItem value="3">
|
||||||
@@ -663,7 +671,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && (
|
||||||
<AccordionItem value="6">
|
<AccordionItem value="6">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Retry Settings</div>
|
<div className={styles.header}>Retry Settings</div>
|
||||||
@@ -695,8 +703,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
<div>
|
<div>
|
||||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
<InfoTooltip className={styles.headerIcon}>
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
|
||||||
part of the response. Default value is 0 milliseconds.
|
as part of the response. Default value is 0 milliseconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
@@ -735,6 +743,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<AccordionItem value="7">
|
<AccordionItem value="7">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -758,7 +767,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
{shouldShowCrossPartitionOption && (
|
{shouldShowCrossPartitionOption && (
|
||||||
<AccordionItem value="8">
|
<AccordionItem value="8">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -784,7 +792,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowParallelismOption && (
|
{shouldShowParallelismOption && (
|
||||||
<AccordionItem value="9">
|
<AccordionItem value="9">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -818,7 +825,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowPriorityLevelOption && (
|
{shouldShowPriorityLevelOption && (
|
||||||
<AccordionItem value="10">
|
<AccordionItem value="10">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -842,7 +848,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowGraphAutoVizOption && (
|
{shouldShowGraphAutoVizOption && (
|
||||||
<AccordionItem value="11">
|
<AccordionItem value="11">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -864,7 +869,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowCopilotSampleDBOption && (
|
{shouldShowCopilotSampleDBOption && (
|
||||||
<AccordionItem value="12">
|
<AccordionItem value="12">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -308,18 +308,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="panelTextBold"
|
className="panelTextBold"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Analytical store
|
|
||||||
</Text>
|
|
||||||
<StyledTooltipHostBase
|
|
||||||
content={
|
|
||||||
<Text
|
<Text
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
@@ -332,17 +325,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
Learn more
|
Learn more
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
</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>
|
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
|
|||||||
@@ -3,17 +3,11 @@
|
|||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import {
|
import {
|
||||||
AppStateComponentNames,
|
AppStateComponentNames,
|
||||||
deleteState,
|
deleteSubComponentState,
|
||||||
loadState,
|
readSubComponentState,
|
||||||
saveState,
|
saveSubComponentState,
|
||||||
saveStateDebounced,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
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 {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
@@ -21,6 +15,7 @@ export enum SubComponentName {
|
|||||||
MainTabDivider = "MainTabDivider",
|
MainTabDivider = "MainTabDivider",
|
||||||
ColumnsSelection = "ColumnsSelection",
|
ColumnsSelection = "ColumnsSelection",
|
||||||
ColumnSort = "ColumnSort",
|
ColumnSort = "ColumnSort",
|
||||||
|
CurrentFilter = "CurrentFilter",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||||
@@ -30,84 +25,22 @@ export type TabDivider = { leftPaneWidthPercent: number };
|
|||||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||||
|
|
||||||
/**
|
// Wrap the ...SubComponentState functions for type safety
|
||||||
*
|
|
||||||
* @param subComponentName
|
export const readDocumentsTabSubComponentState = <T>(
|
||||||
* @param collection
|
|
||||||
* @param defaultValue Will be returned if persisted state is not found
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const readSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
): T => {
|
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
|
||||||
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({
|
export const saveDocumentsTabSubComponentState = <T>(
|
||||||
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>(
|
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
state: T,
|
state: T,
|
||||||
debounce?: boolean,
|
debounce?: boolean,
|
||||||
): void => {
|
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
|
||||||
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)(
|
export const deleteDocumentsTabSubComponentState = (
|
||||||
{
|
subComponentName: SubComponentName,
|
||||||
componentName: componentName,
|
collection: ViewModels.CollectionBase,
|
||||||
subComponentName,
|
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);
|
||||||
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(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ jest.mock("Common/dataAccess/queryDocuments", () => ({
|
|||||||
requestCharge: 1,
|
requestCharge: 1,
|
||||||
activityId: "activityId",
|
activityId: "activityId",
|
||||||
indexMetrics: "indexMetrics",
|
indexMetrics: "indexMetrics",
|
||||||
|
correlatedActivityId: undefined,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
@@ -385,22 +386,6 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
it("should render the page", () => {
|
it("should render the page", () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
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", () => {
|
describe("Command bar buttons", () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarBody,
|
MessageBarBody,
|
||||||
@@ -10,8 +9,7 @@ import {
|
|||||||
makeStyles,
|
makeStyles,
|
||||||
shorthands,
|
shorthands,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { Dismiss16Filled } from "@fluentui/react-icons";
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { createDocument } from "Common/dataAccess/createDocument";
|
import { createDocument } from "Common/dataAccess/createDocument";
|
||||||
@@ -23,9 +21,11 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
|||||||
import { readDocument } from "Common/dataAccess/readDocument";
|
import { readDocument } from "Common/dataAccess/readDocument";
|
||||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
|
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -35,8 +35,9 @@ import {
|
|||||||
FilterHistory,
|
FilterHistory,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
TabDivider,
|
TabDivider,
|
||||||
readSubComponentState,
|
deleteDocumentsTabSubComponentState,
|
||||||
saveSubComponentState,
|
readDocumentsTabSubComponentState,
|
||||||
|
saveDocumentsTabSubComponentState,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
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 =
|
const NO_SQL_THROTTLING_DOC_URL =
|
||||||
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
"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 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;
|
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||||
export const useDocumentsTabStyles = makeStyles({
|
export const useDocumentsTabStyles = makeStyles({
|
||||||
@@ -90,12 +92,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
},
|
},
|
||||||
filterInput: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
appliedFilter: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
tableContainer: {
|
tableContainer: {
|
||||||
marginRight: tokens.spacingHorizontalXXXL,
|
marginRight: tokens.spacingHorizontalXXXL,
|
||||||
},
|
},
|
||||||
@@ -146,6 +142,8 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
private title: string;
|
private title: string;
|
||||||
private resourceTokenPartitionKey: string;
|
private resourceTokenPartitionKey: string;
|
||||||
|
|
||||||
|
protected persistedState: OpenCollectionTab;
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
@@ -153,6 +151,13 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
this.title = options.title;
|
this.title = options.title;
|
||||||
this.partitionKey = options.partitionKey;
|
this.partitionKey = options.partitionKey;
|
||||||
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
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 {
|
public render(): JSX.Element {
|
||||||
@@ -556,8 +561,6 @@ export interface IDocumentsTabComponentProps {
|
|||||||
isTabActive: boolean;
|
isTabActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
|
||||||
|
|
||||||
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
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(
|
['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"`),
|
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
||||||
@@ -583,14 +586,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onIsExecutingChange,
|
onIsExecutingChange,
|
||||||
isTabActive,
|
isTabActive,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true);
|
const [filterContent, setFilterContent] = useState<string>(() =>
|
||||||
const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false);
|
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
|
||||||
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
);
|
||||||
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
|
||||||
const [filterContent, setFilterContent] = useState<string>("");
|
|
||||||
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
const filterInput = useRef<HTMLInputElement>(null);
|
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
@@ -619,7 +620,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -634,7 +635,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// User's filter history
|
// User's filter history
|
||||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
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)
|
// For progress bar for bulk delete (noSql)
|
||||||
@@ -657,12 +658,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFilterFocused) {
|
|
||||||
filterInput.current?.focus();
|
|
||||||
}
|
|
||||||
}, [isFilterFocused]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively delete all documents by retrying throttled requests (429).
|
* 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.
|
* 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);
|
}, timeout);
|
||||||
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
||||||
|
|
||||||
const applyFilterButton = {
|
|
||||||
enabled: true,
|
|
||||||
visible: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKey: DataModels.PartitionKey = useMemo(
|
const partitionKey: DataModels.PartitionKey = useMemo(
|
||||||
() => _partitionKey || (_collection && _collection.partitionKey),
|
() => _partitionKey || (_collection && _collection.partitionKey),
|
||||||
[_collection, _partitionKey],
|
[_collection, _partitionKey],
|
||||||
@@ -787,7 +777,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -831,12 +821,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
// This is executed in onActivate() in the original code.
|
// This is executed in onActivate() in the original code.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKeyboardActions({
|
setKeyboardActions({
|
||||||
[KeyboardAction.SEARCH]: () => {
|
|
||||||
onShowFilterClick();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||||
setFilterContent("");
|
updateFilterContent("");
|
||||||
refreshDocumentsGrid(true);
|
refreshDocumentsGrid(true);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -1317,12 +1303,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onShowFilterClick = () => {
|
|
||||||
setIsFilterCreated(true);
|
|
||||||
setIsFilterExpanded(true);
|
|
||||||
setIsFilterFocused(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryTimeoutEnabled = useCallback(
|
const queryTimeoutEnabled = useCallback(
|
||||||
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||||
[isPreferredApiMongoDB],
|
[isPreferredApiMongoDB],
|
||||||
@@ -1364,19 +1344,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedColumnIds,
|
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 => {
|
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
||||||
setDocumentIds(newDocumentsIds);
|
setDocumentIds(newDocumentsIds);
|
||||||
|
|
||||||
@@ -1518,14 +1485,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === Constants.NormalizedEventKey.Enter) {
|
||||||
onApplyFilterClick();
|
onApplyFilterClick();
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
onHideFilterClick();
|
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
// Suppress the default behavior of the key
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -1697,7 +1659,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -2008,7 +1970,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
setLastFilterContents(limitedLastFilterContents);
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
@@ -2023,10 +1985,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
applyFilterButtonPressed,
|
applyFilterButtonPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
// collapse filter
|
|
||||||
setAppliedFilter(filterContent);
|
|
||||||
setIsFilterExpanded(false);
|
|
||||||
|
|
||||||
// If apply filter is pressed, reset current selected document
|
// If apply filter is pressed, reset current selected document
|
||||||
if (applyFilterButtonPressed) {
|
if (applyFilterButtonPressed) {
|
||||||
setClickedRowIndex(RESET_INDEX);
|
setClickedRowIndex(RESET_INDEX);
|
||||||
@@ -2069,7 +2027,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
setSelectedColumnIds(newSelectedColumnIds);
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
|
||||||
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||||
selectedColumnIds: newSelectedColumnIds,
|
selectedColumnIds: newSelectedColumnIds,
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
});
|
});
|
||||||
@@ -2103,101 +2061,72 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
(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 (
|
return (
|
||||||
<CosmosFluentProvider className={styles.container}>
|
<CosmosFluentProvider className={styles.container}>
|
||||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
<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}>
|
<div className={styles.filterRow}>
|
||||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||||
<Input
|
<InputDataList
|
||||||
ref={filterInput}
|
dropdownOptions={getFilterChoices()}
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
list={`filtersList-${getUniqueId(_collection)}`}
|
|
||||||
className={`filterInput ${styles.filterInput}`}
|
|
||||||
title="Type a query predicate or choose one from the list."
|
|
||||||
placeholder={
|
placeholder={
|
||||||
isPreferredApiMongoDB
|
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., {´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."
|
: "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}
|
value={filterContent}
|
||||||
autoFocus={true}
|
onChange={updateFilterContent}
|
||||||
onKeyDown={onFilterKeyDown}
|
onKeyDown={onFilterKeyDown}
|
||||||
onChange={(e) => setFilterContent(e.target.value)}
|
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||||
onBlur={() => setIsFilterFocused(false)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
|
||||||
{addStringsNoDuplicate(
|
|
||||||
lastFilterContents,
|
|
||||||
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
|
||||||
).map((filter) => (
|
|
||||||
<option key={filter} value={filter} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={onApplyFilterClick}
|
onClick={() => {
|
||||||
disabled={!applyFilterButton.enabled}
|
if (isExecuting) {
|
||||||
aria-label="Apply filter"
|
if (!isPreferredApiMongoDB) {
|
||||||
|
queryAbortController.abort();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onApplyFilterClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isExecuting && isPreferredApiMongoDB}
|
||||||
|
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
Apply Filter
|
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* <Split> doesn't like to be a flex child */}
|
|
||||||
<div style={{ overflow: "hidden", height: "100%" }}>
|
|
||||||
<Allotment
|
<Allotment
|
||||||
onDragEnd={(sizes: number[]) => {
|
onDragEnd={(sizes: number[]) => {
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
setTabStateData(tabStateData);
|
setTabStateData(tabStateData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2265,7 +2194,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
</Allotment>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{bulkDeleteOperation && (
|
{bulkDeleteOperation && (
|
||||||
<ProgressModalDialog
|
<ProgressModalDialog
|
||||||
isOpen={isBulkDeleteDialogOpen}
|
isOpen={isBulkDeleteDialogOpen}
|
||||||
|
|||||||
@@ -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) {
|
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||||
let newWrapper;
|
let newWrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan
|
|||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
ColumnSort,
|
ColumnSort,
|
||||||
deleteSubComponentState,
|
deleteDocumentsTabSubComponentState,
|
||||||
readSubComponentState,
|
readDocumentsTabSubComponentState,
|
||||||
saveSubComponentState,
|
saveDocumentsTabSubComponentState,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
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 sortedRowsRef = React.useRef(null);
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
|
||||||
|
SubComponentName.ColumnSizes,
|
||||||
|
collection,
|
||||||
|
{},
|
||||||
|
);
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
selectedColumnIds.forEach((columnId) => {
|
selectedColumnIds.forEach((columnId) => {
|
||||||
if (
|
if (
|
||||||
@@ -142,7 +146,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
sortDirection: "ascending" | "descending";
|
sortDirection: "ascending" | "descending";
|
||||||
sortColumn: TableColumnId | undefined;
|
sortColumn: TableColumnId | undefined;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||||
|
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
return {
|
return {
|
||||||
@@ -174,7 +178,12 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as ColumnSizesMap);
|
}, {} as ColumnSizesMap);
|
||||||
|
|
||||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
saveDocumentsTabSubComponentState<ColumnSizesMap>(
|
||||||
|
SubComponentName.ColumnSizes,
|
||||||
|
collection,
|
||||||
|
persistentSizes,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
return newSizingOptions;
|
return newSizingOptions;
|
||||||
});
|
});
|
||||||
@@ -186,11 +195,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
setColumnSort(event, columnId, direction);
|
setColumnSort(event, columnId, direction);
|
||||||
|
|
||||||
if (columnId === undefined || direction === undefined) {
|
if (columnId === undefined || direction === undefined) {
|
||||||
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
|
||||||
return;
|
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
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
|
|||||||
@@ -19,25 +19,44 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
<span>
|
<span>
|
||||||
SELECT * FROM c
|
SELECT * FROM c
|
||||||
</span>
|
</span>
|
||||||
<span
|
<InputDataList
|
||||||
className="___r7kt3y0_0000000 fqerorx"
|
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
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
|
aria-label="Apply filter"
|
||||||
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
size="small"
|
size="small"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
Edit Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"overflow": "hidden",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Allotment
|
<Allotment
|
||||||
onDragEnd={[Function]}
|
onDragEnd={[Function]}
|
||||||
>
|
>
|
||||||
@@ -117,6 +136,5 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
</Allotment>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ActionType, TabKind } from "Contracts/ActionContracts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import MongoUtility from "../../../Common/MongoUtility";
|
import MongoUtility from "../../../Common/MongoUtility";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
@@ -20,7 +21,7 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
private mongoQueryTabProps: IMongoQueryTabProps,
|
private mongoQueryTabProps: IMongoQueryTabProps,
|
||||||
) {
|
) {
|
||||||
super(options, mongoQueryTabProps);
|
super(options, mongoQueryTabProps);
|
||||||
this.queryText = "";
|
this.queryText = options.queryText ?? "";
|
||||||
this.iMongoQueryTabComponentProps = {
|
this.iMongoQueryTabComponentProps = {
|
||||||
collection: options.collection,
|
collection: options.collection,
|
||||||
isExecutionError: this.isExecutionError(),
|
isExecutionError: this.isExecutionError(),
|
||||||
@@ -28,6 +29,8 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
tabsBaseInstance: this,
|
tabsBaseInstance: this,
|
||||||
queryText: this.queryText,
|
queryText: this.queryText,
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
|
splitterDirection: options.splitterDirection,
|
||||||
|
queryViewSizePercent: options.queryViewSizePercent,
|
||||||
container: this.mongoQueryTabProps.container,
|
container: this.mongoQueryTabProps.container,
|
||||||
onTabAccessor: (instance: ITabAccessor): void => {
|
onTabAccessor: (instance: ITabAccessor): void => {
|
||||||
this.iTabAccessor = instance;
|
this.iTabAccessor = instance;
|
||||||
@@ -35,6 +38,26 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
isPreferredApiMongoDB: true,
|
isPreferredApiMongoDB: true,
|
||||||
monacoEditorSetting: "plaintext",
|
monacoEditorSetting: "plaintext",
|
||||||
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
@@ -26,6 +27,8 @@ export class NewQueryTab extends TabsBase {
|
|||||||
public iQueryTabComponentProps: IQueryTabComponentProps;
|
public iQueryTabComponentProps: IQueryTabComponentProps;
|
||||||
public iTabAccessor: ITabAccessor;
|
public iTabAccessor: ITabAccessor;
|
||||||
|
|
||||||
|
protected persistedState: OpenQueryTab;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
options: QueryTabOptions,
|
options: QueryTabOptions,
|
||||||
private props: IQueryTabProps,
|
private props: IQueryTabProps,
|
||||||
@@ -39,12 +42,41 @@ export class NewQueryTab extends TabsBase {
|
|||||||
tabsBaseInstance: this,
|
tabsBaseInstance: this,
|
||||||
queryText: options.queryText,
|
queryText: options.queryText,
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
|
splitterDirection: options.splitterDirection,
|
||||||
|
queryViewSizePercent: options.queryViewSizePercent,
|
||||||
container: this.props.container,
|
container: this.props.container,
|
||||||
onTabAccessor: (instance: ITabAccessor): void => {
|
onTabAccessor: (instance: ITabAccessor): void => {
|
||||||
this.iTabAccessor = instance;
|
this.iTabAccessor = instance;
|
||||||
},
|
},
|
||||||
isPreferredApiMongoDB: false,
|
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 {
|
public render(): JSX.Element {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|||||||
AppStateComponentNames: {
|
AppStateComponentNames: {
|
||||||
QueryCopilot: "QueryCopilot",
|
QueryCopilot: "QueryCopilot",
|
||||||
},
|
},
|
||||||
|
readSubComponentState: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
|
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
@@ -18,13 +18,7 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
|||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import {
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
LocalStorageUtility,
|
|
||||||
StorageKey,
|
|
||||||
getDefaultQueryResultsView,
|
|
||||||
getRUThreshold,
|
|
||||||
ruThresholdEnabled,
|
|
||||||
} from "Shared/StorageUtility";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -99,6 +93,13 @@ export interface IQueryTabComponentProps {
|
|||||||
copilotEnabled?: boolean;
|
copilotEnabled?: boolean;
|
||||||
isSampleCopilotActive?: boolean;
|
isSampleCopilotActive?: boolean;
|
||||||
copilotStore?: Partial<QueryCopilotState>;
|
copilotStore?: Partial<QueryCopilotState>;
|
||||||
|
splitterDirection?: "horizontal" | "vertical";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
|
onUpdatePersistedState: (state: {
|
||||||
|
queryText: string;
|
||||||
|
splitterDirection: "vertical" | "horizontal";
|
||||||
|
queryViewSizePercent: number;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueryTabStates {
|
interface IQueryTabStates {
|
||||||
@@ -118,11 +119,13 @@ interface IQueryTabStates {
|
|||||||
queryResultsView: SplitterDirection;
|
queryResultsView: SplitterDirection;
|
||||||
errors?: QueryError[];
|
errors?: QueryError[];
|
||||||
modelMarkers?: monaco.editor.IMarkerData[];
|
modelMarkers?: monaco.editor.IMarkerData[];
|
||||||
|
queryViewSizePercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const copilotStore = useCopilotStore();
|
const copilotStore = useCopilotStore();
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const queryTabProps = {
|
const queryTabProps = {
|
||||||
...props,
|
...props,
|
||||||
@@ -132,12 +135,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
|
|||||||
isSampleCopilotActive: isSampleCopilotActive,
|
isSampleCopilotActive: isSampleCopilotActive,
|
||||||
copilotStore: copilotStore,
|
copilotStore: copilotStore,
|
||||||
};
|
};
|
||||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
|
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
|
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
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).
|
// 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> {
|
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
||||||
|
private static readonly DEBOUNCE_DELAY_MS = 1000;
|
||||||
|
|
||||||
public queryEditorId: string;
|
public queryEditorId: string;
|
||||||
public executeQueryButton: Button;
|
public executeQueryButton: Button;
|
||||||
public saveQueryButton: Button;
|
public saveQueryButton: Button;
|
||||||
@@ -157,10 +162,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _iterator: MinimalQueryIterator;
|
private _iterator: MinimalQueryIterator;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
queryEditor: React.RefObject<EditorReact>;
|
queryEditor: React.RefObject<EditorReact>;
|
||||||
|
private timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
constructor(props: QueryTabComponentImplProps) {
|
constructor(props: QueryTabComponentImplProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.queryEditor = createRef<EditorReact>();
|
this.queryEditor = createRef<EditorReact>();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -176,7 +181,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
cancelQueryTimeoutID: undefined,
|
cancelQueryTimeoutID: undefined,
|
||||||
copilotActive: this._queryCopilotActive(),
|
copilotActive: this._queryCopilotActive(),
|
||||||
currentTabActive: true,
|
currentTabActive: true,
|
||||||
queryResultsView: getDefaultQueryResultsView(),
|
queryResultsView:
|
||||||
|
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
|
||||||
|
queryViewSizePercent: props.queryViewSizePercent,
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
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 {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
@@ -344,19 +368,19 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let queryOperationOptions: QueryOperationOptions;
|
// let queryOperationOptions: QueryOperationOptions;
|
||||||
if (userContext.apiType === "SQL" && ruThresholdEnabled()) {
|
// if (userContext.apiType === "SQL" && ruThresholdEnabled()) {
|
||||||
const ruThreshold: number = getRUThreshold();
|
// const ruThreshold: number = getRUThreshold();
|
||||||
queryOperationOptions = {
|
// queryOperationOptions = {
|
||||||
ruCapPerOperation: ruThreshold,
|
// ruCapPerOperation: ruThreshold,
|
||||||
} as QueryOperationOptions;
|
// } as QueryOperationOptions;
|
||||||
}
|
// }
|
||||||
const queryDocuments = async (firstItemIndex: number) =>
|
const queryDocuments = async (firstItemIndex: number) =>
|
||||||
await queryDocumentsPage(
|
await queryDocumentsPage(
|
||||||
this.props.collection && this.props.collection.id(),
|
this.props.collection && this.props.collection.id(),
|
||||||
this._iterator,
|
this._iterator,
|
||||||
firstItemIndex,
|
firstItemIndex,
|
||||||
queryOperationOptions,
|
// queryOperationOptions,
|
||||||
);
|
);
|
||||||
this.props.tabsBaseInstance.isExecuting(true);
|
this.props.tabsBaseInstance.isExecuting(true);
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -567,7 +591,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
private _setViewLayout(direction: SplitterDirection): void {
|
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
|
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -599,13 +623,16 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
if (this.state.copilotActive) {
|
if (this.state.copilotActive) {
|
||||||
this.props.copilotStore?.setQuery(newContent);
|
this.props.copilotStore?.setQuery(newContent);
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
sqlQueryEditorContent: newContent,
|
sqlQueryEditorContent: newContent,
|
||||||
queryCopilotGeneratedQuery: "",
|
queryCopilotGeneratedQuery: "",
|
||||||
|
|
||||||
// Clear the markers when the user edits the document.
|
// Clear the markers when the user edits the document.
|
||||||
modelMarkers: [],
|
modelMarkers: [],
|
||||||
});
|
},
|
||||||
|
() => this.saveQueryTabStateDebounced(),
|
||||||
|
);
|
||||||
if (this.isPreferredApiMongoDB) {
|
if (this.isPreferredApiMongoDB) {
|
||||||
if (newContent.length > 0) {
|
if (newContent.length > 0) {
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
@@ -704,8 +731,20 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
></QueryCopilotPromptbar>
|
></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 */}
|
{/* 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
|
||||||
<Allotment.Pane data-test="QueryTab/EditorPane">
|
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
|
<EditorReact
|
||||||
ref={this.queryEditor}
|
ref={this.queryEditor}
|
||||||
className={this.props.styles.queryEditor}
|
className={this.props.styles.queryEditor}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||||
@@ -10,6 +11,18 @@ export class SettingsTabV2 extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
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 {
|
public onActivate(): void {
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||||
|
|||||||
@@ -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 { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { IpRule } from "Contracts/DataModels";
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -35,7 +33,7 @@ interface TabsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||||
const [
|
const [
|
||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
@@ -60,29 +58,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<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 && (
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -343,7 +318,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||||
if (
|
if (
|
||||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
|
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||||
(userContext.apiType === "Cassandra" &&
|
(userContext.apiType === "Cassandra" &&
|
||||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||||
ipRules?.length
|
ipRules?.length
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OpenTab } from "Contracts/ActionContracts";
|
||||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -30,6 +31,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
protected _theme: string;
|
protected _theme: string;
|
||||||
public onLoadStartKey: number;
|
public onLoadStartKey: number;
|
||||||
|
|
||||||
|
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
super();
|
super();
|
||||||
this.index = options.index;
|
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 {
|
public onCloseTabButtonClick(): void {
|
||||||
useTabs.getState().closeTab(this);
|
useTabs.getState().closeTab(this);
|
||||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public partitionKeyProperties: string[];
|
public partitionKeyProperties: string[];
|
||||||
public id: ko.Observable<string>;
|
public id: ko.Observable<string>;
|
||||||
public defaultTtl: ko.Observable<number>;
|
public defaultTtl: ko.Observable<number>;
|
||||||
|
public vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
||||||
|
public fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
||||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
public usageSizeInKB: ko.Observable<number>;
|
public usageSizeInKB: ko.Observable<number>;
|
||||||
@@ -110,6 +112,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.defaultTtl = ko.observable(data.defaultTtl);
|
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.indexingPolicy = ko.observable(data.indexingPolicy);
|
||||||
this.usageSizeInKB = ko.observable();
|
this.usageSizeInKB = ko.observable();
|
||||||
this.offer = 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 collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
const title = "Query " + id;
|
const title = "Query " + id;
|
||||||
@@ -649,13 +659,21 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
queryText: queryText,
|
queryText: queryText,
|
||||||
partitionKey: collection.partitionKey,
|
partitionKey: collection.partitionKey,
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
|
splitterDirection,
|
||||||
|
queryViewSizePercent,
|
||||||
},
|
},
|
||||||
{ container: this.container },
|
{ 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 collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
|
|
||||||
@@ -677,6 +695,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
node: this,
|
node: this,
|
||||||
partitionKey: collection.partitionKey,
|
partitionKey: collection.partitionKey,
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
|
queryText,
|
||||||
|
splitterDirection,
|
||||||
|
queryViewSizePercent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
container: this.container,
|
container: this.container,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { initializeIcons, Link, Text } from "@fluentui/react";
|
import { Link, Text } from "@fluentui/react";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
@@ -20,7 +20,7 @@ const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
|
|||||||
const onInit = async () => {
|
const onInit = async () => {
|
||||||
const dataExplorerUrl = new URL("./", window.location.href).href;
|
const dataExplorerUrl = new URL("./", window.location.href).href;
|
||||||
|
|
||||||
initializeIcons();
|
// initializeIcons();
|
||||||
await initializeConfiguration();
|
await initializeConfiguration();
|
||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { initializeIcons } from "@fluentui/react";
|
// import { initializeIcons } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
|
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -22,7 +22,7 @@ import { useAADAuth } from "./hooks/useAADAuth";
|
|||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
||||||
|
|
||||||
initializeIcons();
|
// initializeIcons();
|
||||||
|
|
||||||
const App: React.FunctionComponent = () => {
|
const App: React.FunctionComponent = () => {
|
||||||
// For handling encrypted portal tokens sent via query paramter
|
// For handling encrypted portal tokens sent via query paramter
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import "./ReactDevTools";
|
import "./ReactDevTools";
|
||||||
|
|
||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
import { loadTheme } from "@fluentui/react";
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
@@ -62,7 +62,7 @@ import "./Shared/appInsights";
|
|||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
|
|
||||||
initializeIcons();
|
// initializeIcons();
|
||||||
|
|
||||||
const App: React.FunctionComponent = () => {
|
const App: React.FunctionComponent = () => {
|
||||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { initializeIcons } from "@fluentui/react";
|
// import { initializeIcons } from "@fluentui/react";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
@@ -14,7 +14,7 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
|||||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||||
|
|
||||||
const onInit = async () => {
|
const onInit = async () => {
|
||||||
initializeIcons();
|
// initializeIcons();
|
||||||
await initializeConfiguration();
|
await initializeConfiguration();
|
||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
readonly restoreTabs: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -108,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
|
restoreTabs: "true" === get("restoretabs"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { initializeIcons, Spinner, SpinnerSize } from "@fluentui/react";
|
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { withTranslation } from "react-i18next";
|
import { withTranslation } from "react-i18next";
|
||||||
@@ -13,7 +13,7 @@ import "./SelfServe.less";
|
|||||||
import { SelfServeComponent } from "./SelfServeComponent";
|
import { SelfServeComponent } from "./SelfServeComponent";
|
||||||
import { SelfServeDescriptor } from "./SelfServeTypes";
|
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||||
import { SelfServeType } from "./SelfServeUtils";
|
import { SelfServeType } from "./SelfServeUtils";
|
||||||
initializeIcons();
|
// initializeIcons();
|
||||||
|
|
||||||
const loadTranslationFile = async (className: string): Promise<void> => {
|
const loadTranslationFile = async (className: string): Promise<void> => {
|
||||||
const language = i18n.languages[0];
|
const language = i18n.languages[0];
|
||||||
|
|||||||
@@ -266,7 +266,10 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
apiVersion: "2023-05-01-preview",
|
apiVersion: "2023-05-01-preview",
|
||||||
queryParams: {
|
queryParams: {
|
||||||
filter: "armRegionName eq '" + regionShortName + "'",
|
filter:
|
||||||
|
"armRegionName eq '" +
|
||||||
|
regionShortName +
|
||||||
|
"' and productDisplayName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
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.
|
// The component name whose state is being saved. Component name must not include special characters.
|
||||||
export enum AppStateComponentNames {
|
export enum AppStateComponentNames {
|
||||||
DocumentsTab = "DocumentsTab",
|
DocumentsTab = "DocumentsTab",
|
||||||
MostRecentActivity = "MostRecentActivity",
|
MostRecentActivity = "MostRecentActivity",
|
||||||
QueryCopilot = "QueryCopilot",
|
QueryCopilot = "QueryCopilot",
|
||||||
|
DataExplorerAction = "DataExplorerAction",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subcomponent for DataExplorerAction
|
||||||
|
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
|
||||||
|
|
||||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
||||||
const SCHEMA_VERSION = 1;
|
const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
@@ -72,12 +80,18 @@ export const hasState = (path: StorePath): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// This is for high-frequency state changes
|
// 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 => {
|
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
||||||
|
const key = createKeyFromPath(path);
|
||||||
|
const timeoutId = pathToTimeoutIdMap.get(key);
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
|
pathToTimeoutIdMap.set(
|
||||||
|
key,
|
||||||
|
setTimeout(() => saveState(path, state), debounceDelayMs),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ApplicationState {
|
interface ApplicationState {
|
||||||
@@ -112,3 +126,93 @@ export const createKeyFromPath = (path: StorePath): string => {
|
|||||||
export const deleteAllStates = (): void => {
|
export const deleteAllStates = (): void => {
|
||||||
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
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(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export interface UserContext {
|
|||||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||||
readonly dataPlaneRbacEnabled?: boolean;
|
readonly dataPlaneRbacEnabled?: boolean;
|
||||||
readonly hasDataPlaneRbacSettingChanged?: boolean;
|
readonly refreshCosmosClient?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ export const isServerlessAccount = (): boolean => {
|
|||||||
export const isVectorSearchEnabled = (): boolean => {
|
export const isVectorSearchEnabled = (): boolean => {
|
||||||
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
|
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFullTextSearchEnabled = (): boolean => {
|
||||||
|
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearch);
|
||||||
|
};
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1237,6 +1237,7 @@ export interface SqlContainerResource {
|
|||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
|
fullTextPolicy?: FullTextPolicy;
|
||||||
|
|
||||||
/* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */
|
/* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
@@ -1281,6 +1282,28 @@ export interface VectorEmbedding {
|
|||||||
distanceFunction?: string;
|
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 */
|
/* Cosmos DB indexing policy */
|
||||||
export interface IndexingPolicy {
|
export interface IndexingPolicy {
|
||||||
/* Indicates if the indexing policy is automatic */
|
/* Indicates if the indexing policy is automatic */
|
||||||
@@ -1301,6 +1324,8 @@ export interface IndexingPolicy {
|
|||||||
spatialIndexes?: SpatialSpec[];
|
spatialIndexes?: SpatialSpec[];
|
||||||
|
|
||||||
vectorIndexes?: VectorIndex[];
|
vectorIndexes?: VectorIndex[];
|
||||||
|
|
||||||
|
fullTextIndexes?: FullTextIndex[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorIndex {
|
export interface VectorIndex {
|
||||||
@@ -1308,6 +1333,11 @@ export interface VectorIndex {
|
|||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FullTextIndex {
|
||||||
|
/** The path in the JSON document to index. */
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/* undocumented */
|
/* undocumented */
|
||||||
export interface ExcludedPath {
|
export interface ExcludedPath {
|
||||||
/* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */
|
/* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ import Explorer from "Explorer/Explorer";
|
|||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
|
readSubComponentState,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -80,6 +84,11 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
await updateContextForCopilot(explorer);
|
await updateContextForCopilot(explorer);
|
||||||
await updateContextForSampleData(explorer);
|
await updateContextForSampleData(explorer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.features.restoreTabs) {
|
||||||
|
restoreOpenTabs();
|
||||||
|
}
|
||||||
|
|
||||||
setExplorer(explorer);
|
setExplorer(explorer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,7 +141,7 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
await scheduleRefreshDatabaseResourceToken(true);
|
await scheduleRefreshDatabaseResourceToken(true);
|
||||||
resolve(explorer);
|
resolve(explorer);
|
||||||
await explorer.refreshAllDatabases();
|
await explorer.refreshAllDatabases();
|
||||||
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
|
if (userContext.fabricContext.isVisible) {
|
||||||
firstContainerOpened = true;
|
firstContainerOpened = true;
|
||||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||||
}
|
}
|
||||||
@@ -429,6 +438,7 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
useTabs.getState().closeAllTabs();
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
@@ -731,8 +741,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNetworkSettingsWarningMessage(useTabs.getState().setNetworkSettingsWarning);
|
|
||||||
|
|
||||||
if (inputs.features) {
|
if (inputs.features) {
|
||||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||||
}
|
}
|
||||||
@@ -816,3 +824,17 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
|||||||
interface SampledataconnectionResponse {
|
interface SampledataconnectionResponse {
|
||||||
connectionString: string;
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { clamp } from "@fluentui/react";
|
import { clamp } from "@fluentui/react";
|
||||||
|
import { OpenTab } from "Contracts/ActionContracts";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
|
saveSubComponentState,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||||
@@ -12,7 +18,6 @@ export interface TabsState {
|
|||||||
openedReactTabs: ReactTabKind[];
|
openedReactTabs: ReactTabKind[];
|
||||||
activeTab: TabsBase | undefined;
|
activeTab: TabsBase | undefined;
|
||||||
activeReactTab: ReactTabKind | undefined;
|
activeReactTab: ReactTabKind | undefined;
|
||||||
networkSettingsWarning: string;
|
|
||||||
queryCopilotTabInitialInput: string;
|
queryCopilotTabInitialInput: string;
|
||||||
isTabExecuting: boolean;
|
isTabExecuting: boolean;
|
||||||
isQueryErrorThrown: boolean;
|
isQueryErrorThrown: boolean;
|
||||||
@@ -27,7 +32,6 @@ export interface TabsState {
|
|||||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||||
setNetworkSettingsWarning: (warningMessage: string) => void;
|
|
||||||
setQueryCopilotTabInitialInput: (input: string) => void;
|
setQueryCopilotTabInitialInput: (input: string) => void;
|
||||||
setIsTabExecuting: (state: boolean) => void;
|
setIsTabExecuting: (state: boolean) => void;
|
||||||
setIsQueryErrorThrown: (state: boolean) => void;
|
setIsQueryErrorThrown: (state: boolean) => void;
|
||||||
@@ -36,6 +40,8 @@ export interface TabsState {
|
|||||||
selectLeftTab: () => void;
|
selectLeftTab: () => void;
|
||||||
selectRightTab: () => void;
|
selectRightTab: () => void;
|
||||||
closeActiveTab: () => void;
|
closeActiveTab: () => void;
|
||||||
|
closeAllTabs: () => void;
|
||||||
|
persistTabsState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReactTabKind {
|
export enum ReactTabKind {
|
||||||
@@ -61,7 +67,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
|
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
|
||||||
activeTab: undefined,
|
activeTab: undefined,
|
||||||
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
|
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
|
||||||
networkSettingsWarning: "",
|
|
||||||
queryCopilotTabInitialInput: "",
|
queryCopilotTabInitialInput: "",
|
||||||
isTabExecuting: false,
|
isTabExecuting: false,
|
||||||
isQueryErrorThrown: false,
|
isQueryErrorThrown: false,
|
||||||
@@ -73,7 +78,9 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
},
|
},
|
||||||
activateNewTab: (tab: TabsBase): void => {
|
activateNewTab: (tab: TabsBase): void => {
|
||||||
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
|
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
|
||||||
|
tab.triggerPersistState = get().persistTabsState;
|
||||||
tab.onActivate();
|
tab.onActivate();
|
||||||
|
get().persistTabsState();
|
||||||
},
|
},
|
||||||
activateReactTab: (tabKind: ReactTabKind): void => {
|
activateReactTab: (tabKind: ReactTabKind): void => {
|
||||||
// Clear the selected node when switching to a react tab.
|
// 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 });
|
set({ openedTabs: updatedTabs });
|
||||||
|
|
||||||
|
get().persistTabsState();
|
||||||
},
|
},
|
||||||
closeAllNotebookTabs: (hardClose): void => {
|
closeAllNotebookTabs: (hardClose): void => {
|
||||||
const isNotebook = (tabKind: CollectionTabKind): boolean => {
|
const isNotebook = (tabKind: CollectionTabKind): boolean => {
|
||||||
@@ -178,7 +187,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
|
|
||||||
set({ openedReactTabs: updatedOpenedReactTabs });
|
set({ openedReactTabs: updatedOpenedReactTabs });
|
||||||
},
|
},
|
||||||
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
|
|
||||||
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
|
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
|
||||||
setIsTabExecuting: (state: boolean) => {
|
setIsTabExecuting: (state: boolean) => {
|
||||||
set({ isTabExecuting: state });
|
set({ isTabExecuting: state });
|
||||||
@@ -226,4 +234,18 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
state.closeTab(state.activeTab);
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { initializeIcons } from "@fluentui/react";
|
// import { initializeIcons } from "@fluentui/react";
|
||||||
import { configure } from "enzyme";
|
import { configure } from "enzyme";
|
||||||
import Adapter from "enzyme-adapter-react-16";
|
import Adapter from "enzyme-adapter-react-16";
|
||||||
import "jest-canvas-mock";
|
import "jest-canvas-mock";
|
||||||
import enableHooks from "jest-react-hooks-shallow";
|
import enableHooks from "jest-react-hooks-shallow";
|
||||||
import { TextDecoder, TextEncoder } from "util";
|
import { TextDecoder, TextEncoder } from "util";
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
initializeIcons();
|
// initializeIcons();
|
||||||
|
|
||||||
if (typeof window.URL.createObjectURL === "undefined") {
|
if (typeof window.URL.createObjectURL === "undefined") {
|
||||||
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });
|
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });
|
||||||
|
|||||||
@@ -193,7 +193,12 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (argv.analyze) {
|
if (argv.analyze) {
|
||||||
plugins.push(new BundleAnalyzerPlugin());
|
plugins.push(
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: "static",
|
||||||
|
reportFilename: "bundle-report.html",
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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,
|
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
|
// Hack since it is hard to disable watch entirely with webpack dev server https://github.com/webpack/webpack-dev-server/issues/1251#issuecomment-654240734
|
||||||
|
|||||||
Reference in New Issue
Block a user