mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 03:57:31 +00:00
Compare commits
92 Commits
ashleyst/m
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a1ca321d | ||
|
|
d12f10afd8 | ||
|
|
80b926214b | ||
|
|
070b7a4ca7 | ||
|
|
d61ff5dcb5 | ||
|
|
d42eebaa5a | ||
|
|
056be2a74d | ||
|
|
b93c90e7d1 | ||
|
|
82de81f2b6 | ||
|
|
236f075cf6 | ||
|
|
d478af3869 | ||
|
|
93c1fdc238 | ||
|
|
d562fc0f40 | ||
|
|
808faa9fa5 | ||
|
|
c1bc11d27d | ||
|
|
b456e53b2f | ||
|
|
ac2e2a6f8e | ||
|
|
3138580eae | ||
|
|
de5ba041e9 | ||
|
|
aa88815c6e | ||
|
|
ae7184f7ea | ||
|
|
5a2f78b51e | ||
|
|
3a6769280b | ||
|
|
fbc2e1335b | ||
|
|
4768ba3642 | ||
|
|
eb0d7b71b3 | ||
|
|
261289b031 | ||
|
|
fae4589427 | ||
|
|
cbcb7e6240 | ||
|
|
e0b773d920 | ||
|
|
9ec2cea95c | ||
|
|
1a4f713a79 | ||
|
|
7128133874 | ||
|
|
053dc9d76b | ||
|
|
23b2e59560 | ||
|
|
869d81dfbc | ||
|
|
42a1c6c319 | ||
|
|
9f1cc4cd5c | ||
|
|
bd564c665b | ||
|
|
78154bd976 | ||
|
|
91649d2f52 | ||
|
|
d7647b2ecf | ||
|
|
2c7e788358 | ||
|
|
fdbbbd7378 | ||
|
|
82bdeff158 | ||
|
|
825a5d5257 | ||
|
|
d75553a94d | ||
|
|
50c47a82d6 | ||
|
|
2c2f0c8d7b | ||
|
|
cfc8196c4b | ||
|
|
87024f4bf4 | ||
|
|
fe9730206e | ||
|
|
7e5c6420ad | ||
|
|
89a3a040d8 | ||
|
|
ec3afa0526 | ||
|
|
4176a8a9a9 | ||
|
|
bc8094f44f | ||
|
|
d7825f4f78 | ||
|
|
e51c28c634 | ||
|
|
2b84af60f4 | ||
|
|
29a1a819c3 | ||
|
|
5a16eec29d | ||
|
|
2b11e0e52b | ||
|
|
89374a16ba | ||
|
|
dc289ece75 | ||
|
|
1ddd372c6d | ||
|
|
2740657b4a | ||
|
|
8c888a751c | ||
|
|
8140f0edb1 | ||
|
|
ab5239df09 | ||
|
|
3e48393fbb | ||
|
|
0079a9147f | ||
|
|
912688dc14 | ||
|
|
8849526fab | ||
|
|
24af64a66d | ||
|
|
be871737ad | ||
|
|
4d8bb5c3ea | ||
|
|
10a8505b9a | ||
|
|
ef7c2fe2f7 | ||
|
|
4c7aca95e1 | ||
|
|
2243ad895a | ||
|
|
b2d5f91fe1 | ||
|
|
a712193477 | ||
|
|
5ee411693c | ||
|
|
16c7b2567b | ||
|
|
78d9a0cd8d | ||
|
|
c6ad538559 | ||
|
|
2bc09a6efe | ||
|
|
d3a3033b25 | ||
|
|
6bdc714e11 | ||
|
|
5042f28229 | ||
|
|
e1430fd06f |
16
.devcontainer/Dockerfile
Normal file
16
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
|
||||||
|
|
||||||
|
# Install pre-reqs for gyp, and 'canvas' npm module
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
make \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
python3-minimal \
|
||||||
|
libcairo2-dev \
|
||||||
|
libpango1.0-dev \
|
||||||
|
&& \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install node-gyp to build native modules
|
||||||
|
RUN npm install -g node-gyp
|
||||||
32
.devcontainer/devcontainer.json
Normal file
32
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||||
|
{
|
||||||
|
"name": "Azure Cosmos DB Explorer",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile"
|
||||||
|
},
|
||||||
|
"onCreateCommand": ".devcontainer/oncreate",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/azure-cli:1": {
|
||||||
|
"version": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||||
|
"installDirectlyFromGitHubRelease": true,
|
||||||
|
"version": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/sshd:1": {
|
||||||
|
"version": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "yarn install",
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
4
.devcontainer/oncreate
Executable file
4
.devcontainer/oncreate
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Install packages once, to prime the node_modules directory.
|
||||||
|
npm ci
|
||||||
10
.github/workflows/ci.yml
vendored
10
.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/
|
||||||
@@ -117,14 +117,14 @@ jobs:
|
|||||||
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"
|
||||||
@@ -141,7 +141,7 @@ jobs:
|
|||||||
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"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
|
|||||||
### Hosted Development (https://cosmos.azure.com)
|
### Hosted Development (https://cosmos.azure.com)
|
||||||
|
|
||||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://cdb-ms-mpac-pbe.cosmos.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||||
|
|
||||||
### Emulator Development
|
### Emulator Development
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
||||||
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://main.documentdb.ext.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://cdb-ms-mpac-pbe.cosmos.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
||||||
<h3>Emulator Development</h3>
|
<h3>Emulator Development</h3>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -3117,3 +3117,7 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebarContainer {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ a:focus {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splashLoaderContainer {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
#divExplorer {
|
#divExplorer {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
@@ -27,26 +31,24 @@ a:focus {
|
|||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
@@ -65,17 +67,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
||||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
||||||
border-bottom: 2px solid @FabricAccentMedium;
|
border-bottom: 2px solid @FabricAccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||||
border-bottom: 0px none transparent;
|
border-bottom: 0px none transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +95,10 @@ a:focus {
|
|||||||
padding-bottom: @SmallSpace;
|
padding-bottom: @SmallSpace;
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
@@ -119,7 +120,6 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -156,25 +156,21 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
&>.treeNodeHeader {
|
& > .treeNodeHeader {
|
||||||
background-color: @FabricAccentExtra;
|
background-color: @FabricAccentExtra;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.filterbtnstyle {
|
.filterbtnstyle {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -200,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%);
|
||||||
}
|
}
|
||||||
|
|||||||
150
package-lock.json
generated
150
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",
|
||||||
@@ -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"
|
||||||
@@ -11708,6 +11736,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"
|
||||||
@@ -19895,6 +19924,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",
|
||||||
@@ -21095,6 +21125,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",
|
||||||
@@ -27067,8 +27098,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 +29769,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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const port = process.env.PORT || 3000;
|
|||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
const api = createProxyMiddleware("/api", {
|
const api = createProxyMiddleware("/api", {
|
||||||
target: "https://main.documentdb.ext.azure.com",
|
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
bypass: (req, res) => {
|
bypass: (req, res) => {
|
||||||
@@ -16,7 +16,7 @@ const api = createProxyMiddleware("/api", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const proxy = createProxyMiddleware("/proxy", {
|
const proxy = createProxyMiddleware("/proxy", {
|
||||||
target: "https://main.documentdb.ext.azure.com",
|
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -136,6 +137,7 @@ export class BackendApi {
|
|||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
||||||
|
public static readonly SampleData: string = "SampleData";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
@@ -154,6 +156,18 @@ export class MongoProxyEndpoints {
|
|||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MongoProxyApi {
|
||||||
|
public static readonly ResourceList: string = "ResourceList";
|
||||||
|
public static readonly QueryDocuments: string = "QueryDocuments";
|
||||||
|
public static readonly CreateDocument: string = "CreateDocument";
|
||||||
|
public static readonly ReadDocument: string = "ReadDocument";
|
||||||
|
public static readonly UpdateDocument: string = "UpdateDocument";
|
||||||
|
public static readonly DeleteDocument: string = "DeleteDocument";
|
||||||
|
public static readonly CreateCollectionWithProxy: string = "CreateCollectionWithProxy";
|
||||||
|
public static readonly LegacyMongoShell: string = "LegacyMongoShell";
|
||||||
|
public static readonly BulkDelete: string = "BulkDelete";
|
||||||
|
}
|
||||||
|
|
||||||
export class CassandraProxyEndpoints {
|
export class CassandraProxyEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7240";
|
public static readonly Development: string = "https://localhost:7240";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||||
@@ -292,6 +306,7 @@ export class HttpStatusCodes {
|
|||||||
public static readonly Accepted: number = 202;
|
public static readonly Accepted: number = 202;
|
||||||
public static readonly NoContent: number = 204;
|
public static readonly NoContent: number = 204;
|
||||||
public static readonly NotModified: number = 304;
|
public static readonly NotModified: number = 304;
|
||||||
|
public static readonly BadRequest: number = 400;
|
||||||
public static readonly Unauthorized: number = 401;
|
public static readonly Unauthorized: number = 401;
|
||||||
public static readonly Forbidden: number = 403;
|
public static readonly Forbidden: number = 403;
|
||||||
public static readonly NotFound: number = 404;
|
public static readonly NotFound: number = 404;
|
||||||
@@ -503,7 +518,7 @@ export class PriorityLevel {
|
|||||||
public static readonly Default = "low";
|
public static readonly Default = "low";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
export const QueryCopilotSampleContainerSchema = {
|
export const QueryCopilotSampleContainerSchema = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
import { PortalBackendEndpoints } from "Common/Constants";
|
||||||
|
import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
||||||
|
|
||||||
@@ -20,22 +21,22 @@ describe("getTokenFromAuthService", () => {
|
|||||||
|
|
||||||
it("builds the correct URL in production", () => {
|
it("builds the correct URL in production", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
});
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens",
|
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL in dev", () => {
|
it("builds the correct URL in dev", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
||||||
});
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -78,7 +79,7 @@ describe("requestPlugin", () => {
|
|||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
platform: Platform.Hosted,
|
platform: Platform.Hosted,
|
||||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ 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";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
|
import { runCommand } from "hooks/useDatabaseAccounts";
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -27,11 +28,12 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
);
|
);
|
||||||
if (!userContext.aadToken) {
|
if (!userContext.aadToken) {
|
||||||
logConsoleError(
|
logConsoleError(
|
||||||
`AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`,
|
`AAD token does not exist. Please use the "Login for Entra ID" button in the Toolbar prior to performing Entra ID RBAC operations`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||||
|
console.log("AAD Token ", userContext.aadToken);
|
||||||
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||||
return authorizationToken;
|
return authorizationToken;
|
||||||
}
|
}
|
||||||
@@ -209,10 +211,14 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
|
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wrappedTokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
|
return await runCommand(tokenProvider, requestInfo);
|
||||||
|
};
|
||||||
|
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||||
key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey,
|
key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey,
|
||||||
tokenProvider,
|
tokenProvider: wrappedTokenProvider,
|
||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
defaultHeaders: _defaultHeaders,
|
defaultHeaders: _defaultHeaders,
|
||||||
connectionPolicy: {
|
connectionPolicy: {
|
||||||
|
|||||||
3
src/Common/DatabaseUtility.ts
Normal file
3
src/Common/DatabaseUtility.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function getNewDatabaseSharedThroughputDefault(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export interface TableEntityProps {
|
|||||||
isEntityValueDisable?: boolean;
|
isEntityValueDisable?: boolean;
|
||||||
entityTimeValue: string;
|
entityTimeValue: string;
|
||||||
entityValueType: string;
|
entityValueType: string;
|
||||||
|
entityProperty: string;
|
||||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
onSelectDate: (date: Date | null | undefined) => void;
|
onSelectDate: (date: Date | null | undefined) => void;
|
||||||
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
@@ -26,6 +27,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
onSelectDate,
|
onSelectDate,
|
||||||
isEntityValueDisable,
|
isEntityValueDisable,
|
||||||
onEntityTimeValueChange,
|
onEntityTimeValueChange,
|
||||||
|
entityProperty,
|
||||||
}: TableEntityProps): JSX.Element => {
|
}: TableEntityProps): JSX.Element => {
|
||||||
if (isEntityTypeDate) {
|
if (isEntityTypeDate) {
|
||||||
return (
|
return (
|
||||||
@@ -51,15 +53,20 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<>
|
||||||
label={entityValueLabel && entityValueLabel}
|
<span id={entityProperty} className="screenReaderOnly">
|
||||||
className="addEntityTextField"
|
Edit Property {entityProperty} {attributeValueLabel}
|
||||||
disabled={isEntityValueDisable}
|
</span>
|
||||||
type={entityValueType}
|
<TextField
|
||||||
placeholder={entityValuePlaceholder}
|
label={entityValueLabel && entityValueLabel}
|
||||||
value={typeof entityValue === "string" ? entityValue : ""}
|
className="addEntityTextField"
|
||||||
onChange={onEntityValueChange}
|
disabled={isEntityValueDisable}
|
||||||
ariaLabel={attributeValueLabel}
|
type={entityValueType}
|
||||||
/>
|
placeholder={entityValuePlaceholder}
|
||||||
|
value={typeof entityValue === "string" ? entityValue : ""}
|
||||||
|
onChange={onEntityValueChange}
|
||||||
|
aria-labelledby={entityProperty}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PortalBackendEndpoints } from "Common/Constants";
|
||||||
|
import { updateConfigContext } from "ConfigContext";
|
||||||
import * as EnvironmentUtility from "./EnvironmentUtility";
|
import * as EnvironmentUtility from "./EnvironmentUtility";
|
||||||
|
|
||||||
describe("Environment Utility Test", () => {
|
describe("Environment Utility Test", () => {
|
||||||
@@ -11,4 +13,18 @@ describe("Environment Utility Test", () => {
|
|||||||
const expectedResult = "test/";
|
const expectedResult = "test/";
|
||||||
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Detect environment is Mpac", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||||
|
});
|
||||||
|
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mpac);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Detect environment is Development", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
||||||
|
});
|
||||||
|
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
|
import { PortalBackendEndpoints } from "Common/Constants";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
|
||||||
export function normalizeArmEndpoint(uri: string): string {
|
export function normalizeArmEndpoint(uri: string): string {
|
||||||
if (uri && uri.slice(-1) !== "/") {
|
if (uri && uri.slice(-1) !== "/") {
|
||||||
return `${uri}/`;
|
return `${uri}/`;
|
||||||
}
|
}
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Environment {
|
||||||
|
Development = "Development",
|
||||||
|
Mpac = "MPAC",
|
||||||
|
Prod = "Prod",
|
||||||
|
Fairfax = "Fairfax",
|
||||||
|
Mooncake = "Mooncake",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEnvironment = (): Environment => {
|
||||||
|
const environmentMap: { [key: string]: Environment } = {
|
||||||
|
[PortalBackendEndpoints.Development]: Environment.Development,
|
||||||
|
[PortalBackendEndpoints.Mpac]: Environment.Mpac,
|
||||||
|
[PortalBackendEndpoints.Prod]: Environment.Prod,
|
||||||
|
[PortalBackendEndpoints.Fairfax]: Environment.Fairfax,
|
||||||
|
[PortalBackendEndpoints.Mooncake]: Environment.Mooncake,
|
||||||
|
};
|
||||||
|
|
||||||
|
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
@@ -71,7 +72,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -82,16 +84,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -103,7 +108,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -114,16 +120,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -135,7 +144,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -146,16 +156,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -167,7 +180,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -178,16 +192,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -199,7 +216,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -210,16 +228,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocument(databaseId, collection, documentId);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
deleteDocument(databaseId, collection, documentId);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -231,13 +252,14 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
it("returns a production endpoint", () => {
|
||||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a development endpoint", () => {
|
it("returns a development endpoint", () => {
|
||||||
@@ -249,18 +271,20 @@ describe("MongoProxyClient", () => {
|
|||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.EncryptedToken,
|
authType: AuthType.EncryptedToken,
|
||||||
});
|
});
|
||||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getFeatureEndpointOrDefault", () => {
|
describe("getFeatureEndpointOrDefault", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
});
|
});
|
||||||
const features = extractFeatures(params);
|
const features = extractFeatures(params);
|
||||||
@@ -272,12 +296,12 @@ describe("MongoProxyClient", () => {
|
|||||||
|
|
||||||
it("returns a local endpoint", () => {
|
it("returns a local endpoint", () => {
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
it("returns a production endpoint", () => {
|
||||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
import queryString from "querystring";
|
import queryString from "querystring";
|
||||||
@@ -14,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
|||||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -194,7 +194,7 @@ export function readDocument(
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint("readDocument")) {
|
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -217,7 +217,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -289,7 +289,7 @@ export function createDocument(
|
|||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint("createDocument")) {
|
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -308,7 +308,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -373,7 +373,7 @@ export function updateDocument(
|
|||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint("updateDocument")) {
|
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -396,7 +396,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -464,7 +464,7 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
if (!useMongoProxyEndpoint("deleteDocument")) {
|
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) {
|
||||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -486,7 +486,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
|||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -561,7 +561,10 @@ export function deleteDocuments(
|
|||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
|
||||||
const rids = documentIds.map((documentId) => documentId.id());
|
const rids: string[] = documentIds.map((documentId) => {
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
return idComponents[5];
|
||||||
|
});
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
databaseID: databaseId,
|
databaseID: databaseId,
|
||||||
@@ -572,7 +575,7 @@ export function deleteDocuments(
|
|||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -596,7 +599,7 @@ export function deleteDocuments(
|
|||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -619,7 +622,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -689,12 +692,13 @@ export function getFeatureEndpointOrDefault(feature: string): string {
|
|||||||
if (useMongoProxyEndpoint(feature)) {
|
if (useMongoProxyEndpoint(feature)) {
|
||||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||||
} else {
|
} else {
|
||||||
|
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||||
|
...defaultAllowedMongoProxyEndpoints,
|
||||||
|
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
];
|
||||||
endpoint =
|
endpoint =
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||||
...allowedMongoProxyEndpoints,
|
|
||||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
])
|
|
||||||
? userContext.features.mongoProxyEndpoint
|
? userContext.features.mongoProxyEndpoint
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -715,19 +719,88 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMongoProxyEndpoint(api: string): boolean {
|
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||||
const activeMongoProxyEndpoints: string[] = [
|
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||||
MongoProxyEndpoints.Local,
|
[MongoProxyApi.ResourceList]: [
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Local,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Mooncake,
|
MongoProxyEndpoints.Fairfax,
|
||||||
];
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.QueryDocuments]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.CreateDocument]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.ReadDocument]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.UpdateDocument]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.DeleteDocument]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.LegacyMongoShell]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
[MongoProxyApi.BulkDelete]: [
|
||||||
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
return false;
|
||||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
}
|
||||||
);
|
|
||||||
|
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThrottlingError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
||||||
@@ -739,6 +812,14 @@ async function errorHandling(response: Response, action: string, params: unknown
|
|||||||
if (response.status === HttpStatusCodes.Forbidden) {
|
if (response.status === HttpStatusCodes.Forbidden) {
|
||||||
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||||
return;
|
return;
|
||||||
|
} else if (
|
||||||
|
response.status === HttpStatusCodes.BadRequest &&
|
||||||
|
errorMessage.includes("Error=16500") &&
|
||||||
|
errorMessage.includes("RetryAfterMs=")
|
||||||
|
) {
|
||||||
|
// If throttling is happening, Cosmos DB will return a 400 with a body of:
|
||||||
|
// A write operation resulted in an error. Error=16500, RetryAfterMs=4, Details='Batch write error.
|
||||||
|
throw new ThrottlingError(errorMessage);
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { configContext, Platform } from "../ConfigContext";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
|
||||||
import { userContext } from "../UserContext";
|
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|
||||||
|
|
||||||
const notificationsPath = () => {
|
|
||||||
switch (configContext.platform) {
|
|
||||||
case Platform.Hosted:
|
|
||||||
return "/api/guest/notifications";
|
|
||||||
case Platform.Portal:
|
|
||||||
return "/api/notifications";
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown platform: ${configContext.platform}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
|
||||||
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
|
|
||||||
databaseAccount.name
|
|
||||||
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
|
||||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
|
||||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
|
||||||
|
|
||||||
const response = await window.fetch(url, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(await response.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await response.json()) as DataModels.Notification[];
|
|
||||||
};
|
|
||||||
113
src/Common/QueryError.test.ts
Normal file
113
src/Common/QueryError.test.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import QueryError, { QueryErrorLocation, QueryErrorSeverity } from "Common/QueryError";
|
||||||
|
|
||||||
|
describe("QueryError.tryParse", () => {
|
||||||
|
const testErrorLocationResolver = ({ start, end }: { start: number; end: number }) =>
|
||||||
|
new QueryErrorLocation(
|
||||||
|
{ offset: start, lineNumber: start, column: start },
|
||||||
|
{ offset: end, lineNumber: end, column: end },
|
||||||
|
);
|
||||||
|
|
||||||
|
it("handles a string error", () => {
|
||||||
|
const error = "error";
|
||||||
|
const result = QueryError.tryParse(error, testErrorLocationResolver);
|
||||||
|
expect(result).toEqual([new QueryError("error", QueryErrorSeverity.Error)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles an error object", () => {
|
||||||
|
const error = {
|
||||||
|
message: "error",
|
||||||
|
severity: "Warning",
|
||||||
|
location: { start: 0, end: 1 },
|
||||||
|
code: "code",
|
||||||
|
};
|
||||||
|
const result = QueryError.tryParse(error, testErrorLocationResolver);
|
||||||
|
expect(result).toEqual([
|
||||||
|
new QueryError(
|
||||||
|
"error",
|
||||||
|
QueryErrorSeverity.Warning,
|
||||||
|
"code",
|
||||||
|
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a JSON message without syntax errors", () => {
|
||||||
|
const innerError = {
|
||||||
|
code: "BadRequest",
|
||||||
|
message: "Your query is bad, and you should feel bad",
|
||||||
|
};
|
||||||
|
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||||
|
const outerError = {
|
||||||
|
code: "BadRequest",
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
||||||
|
expect(result).toEqual([
|
||||||
|
new QueryError("Your query is bad, and you should feel bad", QueryErrorSeverity.Error, "BadRequest"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message, along with a prefix and activity id.
|
||||||
|
it("handles single-nested error", () => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
message: "error1",
|
||||||
|
severity: "Warning",
|
||||||
|
location: { start: 0, end: 1 },
|
||||||
|
code: "code1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "error2",
|
||||||
|
severity: "Error",
|
||||||
|
location: { start: 2, end: 3 },
|
||||||
|
code: "code2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const innerError = {
|
||||||
|
code: "BadRequest",
|
||||||
|
message: "Your query is bad, and you should feel bad",
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||||
|
const outerError = {
|
||||||
|
code: "BadRequest",
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
||||||
|
expect(result).toEqual([
|
||||||
|
new QueryError(
|
||||||
|
"error1",
|
||||||
|
QueryErrorSeverity.Warning,
|
||||||
|
"code1",
|
||||||
|
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
|
||||||
|
),
|
||||||
|
new QueryError(
|
||||||
|
"error2",
|
||||||
|
QueryErrorSeverity.Error,
|
||||||
|
"code2",
|
||||||
|
new QueryErrorLocation({ offset: 2, lineNumber: 2, column: 2 }, { offset: 3, lineNumber: 3, column: 3 }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Imitate another value we've gotten from the backend, which has a doubly-nested JSON payload.
|
||||||
|
it("handles double-nested error", () => {
|
||||||
|
const outerError = {
|
||||||
|
code: "BadRequest",
|
||||||
|
message:
|
||||||
|
'{"code":"BadRequest","message":"{\\"errors\\":[{\\"severity\\":\\"Error\\",\\"location\\":{\\"start\\":7,\\"end\\":18},\\"code\\":\\"SC2005\\",\\"message\\":\\"\'nonexistent\' is not a recognized built-in function name.\\"}]}\\r\\nActivityId: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0"}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
||||||
|
expect(result).toEqual([
|
||||||
|
new QueryError(
|
||||||
|
"'nonexistent' is not a recognized built-in function name.",
|
||||||
|
QueryErrorSeverity.Error,
|
||||||
|
"SC2005",
|
||||||
|
new QueryErrorLocation({ offset: 7, lineNumber: 7, column: 7 }, { offset: 18, lineNumber: 18, column: 18 }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getErrorMessage } from "Common/ErrorHandlingUtils";
|
|
||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
|
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||||
|
|
||||||
export enum QueryErrorSeverity {
|
export enum QueryErrorSeverity {
|
||||||
Error = "Error",
|
Error = "Error",
|
||||||
@@ -97,13 +97,44 @@ export const createMonacoMarkersForQueryErrors = (errors: QueryError[]) => {
|
|||||||
.filter((marker) => !!marker);
|
.filter((marker) => !!marker);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ErrorEnrichment {
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
learnMoreUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
|
||||||
|
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
|
||||||
|
if (ruThresholdEnabled()) {
|
||||||
|
const threshold = getRUThreshold();
|
||||||
|
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HELP_LINKS: Record<string, string> = {
|
||||||
|
OPERATION_RU_LIMIT_EXCEEDED:
|
||||||
|
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
|
||||||
|
};
|
||||||
|
|
||||||
export default class QueryError {
|
export default class QueryError {
|
||||||
|
message: string;
|
||||||
|
helpLink?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public message: string,
|
message: string,
|
||||||
public severity: QueryErrorSeverity,
|
public severity: QueryErrorSeverity,
|
||||||
public code?: string,
|
public code?: string,
|
||||||
public location?: QueryErrorLocation,
|
public location?: QueryErrorLocation,
|
||||||
) {}
|
helpLink?: string,
|
||||||
|
) {
|
||||||
|
// Automatically replace the message with a more Data Explorer-specific message if we have for this error code.
|
||||||
|
this.message = REPLACEMENT_MESSAGES[code] ? REPLACEMENT_MESSAGES[code](message) : message;
|
||||||
|
|
||||||
|
// Automatically set the help link if we have one for this error code.
|
||||||
|
this.helpLink = helpLink ?? HELP_LINKS[code];
|
||||||
|
}
|
||||||
|
|
||||||
getMonacoSeverity(): monaco.MarkerSeverity {
|
getMonacoSeverity(): monaco.MarkerSeverity {
|
||||||
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
||||||
@@ -135,7 +166,7 @@ export default class QueryError {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error as string | Error);
|
const errorMessage = error as string;
|
||||||
|
|
||||||
// Map some well known messages to richer errors
|
// Map some well known messages to richer errors
|
||||||
const knownError = knownErrors[errorMessage];
|
const knownError = knownErrors[errorMessage];
|
||||||
@@ -160,7 +191,9 @@ export default class QueryError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const severity =
|
const severity =
|
||||||
"severity" in error && typeof error.severity === "string" ? (error.severity as QueryErrorSeverity) : undefined;
|
"severity" in error && typeof error.severity === "string"
|
||||||
|
? (error.severity as QueryErrorSeverity)
|
||||||
|
: QueryErrorSeverity.Error;
|
||||||
const location =
|
const location =
|
||||||
"location" in error && typeof error.location === "object"
|
"location" in error && typeof error.location === "object"
|
||||||
? locationResolver(error.location as { start: number; end: number })
|
? locationResolver(error.location as { start: number; end: number })
|
||||||
@@ -173,35 +206,49 @@ export default class QueryError {
|
|||||||
error: unknown,
|
error: unknown,
|
||||||
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
||||||
): QueryError[] | null {
|
): QueryError[] | null {
|
||||||
if (typeof error === "object" && "message" in error) {
|
let message: string | undefined;
|
||||||
error = error.message;
|
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
|
||||||
}
|
message = error.message;
|
||||||
|
} else {
|
||||||
if (typeof error !== "string") {
|
// Unsupported error format.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
// Some newer backends produce a message that contains a doubly-nested JSON payload.
|
||||||
let message = error;
|
// In this case, the message we get is a fully-complete JSON object we can parse.
|
||||||
if (message.startsWith("Message: ")) {
|
// So let's try that first
|
||||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
if (message.startsWith("{") && message.endsWith("}")) {
|
||||||
// So we use a separate variable to avoid this.
|
let outer: unknown = undefined;
|
||||||
message = message.substring("Message: ".length);
|
try {
|
||||||
|
outer = JSON.parse(message);
|
||||||
|
if (typeof outer === "object" && "message" in outer && typeof outer.message === "string") {
|
||||||
|
message = outer.message;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Just continue if the parsing fails. We'll use the fallback logic below.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = message.split("\n");
|
const lines = message.split("\n");
|
||||||
message = lines[0].trim();
|
message = lines[0].trim();
|
||||||
|
|
||||||
|
if (message.startsWith("Message: ")) {
|
||||||
|
message = message.substring("Message: ".length);
|
||||||
|
}
|
||||||
|
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(message);
|
parsed = JSON.parse(message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not a query error.
|
// The message doesn't contain a nested error.
|
||||||
return null;
|
return [QueryError.read(error, locationResolver)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof parsed === "object" && "errors" in parsed && Array.isArray(parsed.errors)) {
|
if (typeof parsed === "object") {
|
||||||
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
if ("errors" in parsed && Array.isArray(parsed.errors)) {
|
||||||
|
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
||||||
|
}
|
||||||
|
return [QueryError.read(parsed, locationResolver)];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
onEntityValueChange={onEntityValueChange}
|
onEntityValueChange={onEntityValueChange}
|
||||||
onSelectDate={onSelectDate}
|
onSelectDate={onSelectDate}
|
||||||
onEntityTimeValueChange={onEntityTimeValueChange}
|
onEntityTimeValueChange={onEntityTimeValueChange}
|
||||||
|
entityProperty={entityProperty}
|
||||||
/>
|
/>
|
||||||
{!isEntityValueDisable && (
|
{!isEntityValueDisable && (
|
||||||
<TooltipHost content="Edit property" id="editTooltip">
|
<TooltipHost content="Edit property" id="editTooltip">
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -26,14 +26,23 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IBulkDeleteResult {
|
||||||
|
documentId: DocumentId;
|
||||||
|
requestCharge: number;
|
||||||
|
statusCode: number;
|
||||||
|
retryAfterMilliseconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk delete documents
|
* Bulk delete documents
|
||||||
* @param collection
|
* @param collection
|
||||||
* @param documentId
|
* @param documentId
|
||||||
* @returns array of ids that were successfully deleted
|
* @returns array of results and status codes
|
||||||
*/
|
*/
|
||||||
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
|
export const deleteDocuments = async (
|
||||||
const nbDocuments = documentIds.length;
|
collection: CollectionBase,
|
||||||
|
documentIds: DocumentId[],
|
||||||
|
): Promise<IBulkDeleteResult[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||||
try {
|
try {
|
||||||
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
||||||
@@ -56,18 +65,17 @@ export const deleteDocuments = async (collection: CollectionBase, documentIds: D
|
|||||||
operationType: BulkOperationType.Delete,
|
operationType: BulkOperationType.Delete,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
|
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
||||||
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
|
return bulkResults.map((bulkResult, index) => {
|
||||||
|
const documentId = documentIdsChunk[index];
|
||||||
|
return { ...bulkResult, documentId };
|
||||||
|
});
|
||||||
});
|
});
|
||||||
promiseArray.push(promise);
|
promiseArray.push(promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResult = await Promise.all(promiseArray);
|
const allResult = await Promise.all(promiseArray);
|
||||||
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
||||||
logConsoleInfo(
|
|
||||||
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
|
|
||||||
);
|
|
||||||
// TODO: handle case result.length != nbDocuments
|
|
||||||
return flatAllResult;
|
return flatAllResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedCassandraProxyEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ export interface ConfigContext {
|
|||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
||||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
@@ -49,12 +51,11 @@ export interface ConfigContext {
|
|||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
PORTAL_BACKEND_ENDPOINT?: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT?: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
NEW_MONGO_APIS?: string[];
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT?: string;
|
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
NEW_CASSANDRA_APIS?: string[];
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
@@ -66,6 +67,8 @@ export interface ConfigContext {
|
|||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
|
globallyEnabledCassandraAPIs?: string[];
|
||||||
|
globallyEnabledMongoAPIs?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
@@ -73,9 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
|
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||||
|
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
||||||
allowedParentFrameOrigins: [
|
allowedParentFrameOrigins: [
|
||||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||||
|
`^https:\\/\\/cdb-(ms|ff|mc)-prod-pbe\\.cosmos\\.azure\\.(com|us|cn)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||||
@@ -106,21 +112,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
NEW_MONGO_APIS: [
|
|
||||||
"resourcelist",
|
|
||||||
"queryDocuments",
|
|
||||||
"createDocument",
|
|
||||||
"readDocument",
|
|
||||||
"updateDocument",
|
|
||||||
"deleteDocument",
|
|
||||||
"createCollectionWithProxy",
|
|
||||||
"legacyMongoShell",
|
|
||||||
"bulkdelete",
|
|
||||||
],
|
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
|
globallyEnabledCassandraAPIs: [],
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -164,7 +161,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
|
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +174,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||||
|
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ export interface Database extends TreeNode {
|
|||||||
openAddCollection(database: Database, event: MouseEvent): void;
|
openAddCollection(database: Database, event: MouseEvent): void;
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
loadOffer(): Promise<void>;
|
loadOffer(): Promise<void>;
|
||||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBase extends TreeNode {
|
export interface CollectionBase extends TreeNode {
|
||||||
@@ -127,6 +126,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>;
|
||||||
@@ -191,8 +192,6 @@ export interface Collection extends CollectionBase {
|
|||||||
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||||
|
|
||||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,6 +384,8 @@ export interface DataExplorerInputsFrame {
|
|||||||
databaseAccount: any;
|
databaseAccount: any;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
resourceGroup?: string;
|
resourceGroup?: string;
|
||||||
|
tenantId?: string;
|
||||||
|
userName?: string;
|
||||||
masterKey?: string;
|
masterKey?: string;
|
||||||
hasWriteAccess?: boolean;
|
hasWriteAccess?: boolean;
|
||||||
authorizationToken?: string;
|
authorizationToken?: string;
|
||||||
|
|||||||
@@ -56,13 +56,15 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: () =>
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
useSidePanel
|
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||||
.getState()
|
useSidePanel
|
||||||
.openSidePanel(
|
.getState()
|
||||||
"Delete " + getDatabaseName(),
|
.openSidePanel(
|
||||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
"Delete " + getDatabaseName(),
|
||||||
),
|
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
|
);
|
||||||
|
},
|
||||||
label: `Delete ${getDatabaseName()}`,
|
label: `Delete ${getDatabaseName()}`,
|
||||||
styleClass: "deleteDatabaseMenuItem",
|
styleClass: "deleteDatabaseMenuItem",
|
||||||
});
|
});
|
||||||
@@ -146,14 +148,15 @@ export const createCollectionContextMenuButton = (
|
|||||||
if (configContext.platform !== Platform.Fabric) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: () => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
useSidePanel
|
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||||
.getState()
|
useSidePanel
|
||||||
.openSidePanel(
|
.getState()
|
||||||
"Delete " + getCollectionName(),
|
.openSidePanel(
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
"Delete " + getCollectionName(),
|
||||||
);
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
label: `Delete ${getCollectionName()}`,
|
label: `Delete ${getCollectionName()}`,
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
|
|||||||
@@ -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}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface DialogState {
|
|||||||
textFieldProps?: TextFieldProps,
|
textFieldProps?: TextFieldProps,
|
||||||
primaryButtonDisabled?: boolean,
|
primaryButtonDisabled?: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
showOkModalDialog: (title: string, subText: string) => void;
|
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||||
@@ -83,7 +83,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
textFieldProps,
|
textFieldProps,
|
||||||
primaryButtonDisabled,
|
primaryButtonDisabled,
|
||||||
}),
|
}),
|
||||||
showOkModalDialog: (title: string, subText: string): void =>
|
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps): void =>
|
||||||
get().openDialog({
|
get().openDialog({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title,
|
title,
|
||||||
@@ -94,6 +94,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
get().closeDialog();
|
get().closeDialog();
|
||||||
},
|
},
|
||||||
onSecondaryButtonClick: undefined,
|
onSecondaryButtonClick: undefined,
|
||||||
|
linkProps,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
79
src/Explorer/Controls/ProgressModalDialog.tsx
Normal file
79
src/Explorer/Controls/ProgressModalDialog.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogSurface,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
Field,
|
||||||
|
ProgressBar,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface ProgressModalDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
maxValue: number;
|
||||||
|
value: number;
|
||||||
|
dismissText: string;
|
||||||
|
onDismiss: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
/* mode drives the state of the action buttons
|
||||||
|
* inProgress: Show cancel button
|
||||||
|
* completed: Show close button
|
||||||
|
* aborting: Show cancel button, but disabled
|
||||||
|
* aborted: Show close button
|
||||||
|
*/
|
||||||
|
mode?: "inProgress" | "completed" | "aborting" | "aborted";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component that renders a modal dialog with a progress bar.
|
||||||
|
*/
|
||||||
|
export const ProgressModalDialog: React.FC<ProgressModalDialogProps> = ({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
maxValue,
|
||||||
|
value,
|
||||||
|
dismissText,
|
||||||
|
onCancel,
|
||||||
|
onDismiss,
|
||||||
|
children,
|
||||||
|
mode = "completed",
|
||||||
|
}) => (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(event, data) => {
|
||||||
|
if (!data.open) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogSurface>
|
||||||
|
<DialogBody>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Field validationMessage={message} validationState="none">
|
||||||
|
<ProgressBar max={maxValue} value={value} />
|
||||||
|
</Field>
|
||||||
|
{children}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{mode === "inProgress" || mode === "aborting" ? (
|
||||||
|
<Button appearance="secondary" onClick={onCancel} disabled={mode === "aborting"}>
|
||||||
|
{dismissText}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<DialogTrigger disableButtonEnhancement>
|
||||||
|
<Button appearance="primary">Close</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogSurface>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
@@ -134,7 +134,6 @@ describe("SettingsComponent", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
getPendingThroughputSplitNotification: undefined,
|
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
newCollection.getDatabase = () => newDatabase;
|
newCollection.getDatabase = () => newDatabase;
|
||||||
newCollection.offer = ko.observable(undefined);
|
newCollection.offer = ko.observable(undefined);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -130,7 +137,6 @@ export interface SettingsComponentState {
|
|||||||
conflictResolutionPolicyProcedureBaseline: string;
|
conflictResolutionPolicyProcedureBaseline: string;
|
||||||
isConflictResolutionDirty: boolean;
|
isConflictResolutionDirty: boolean;
|
||||||
|
|
||||||
initialNotification: DataModels.Notification;
|
|
||||||
selectedTab: SettingsV2TabTypes;
|
selectedTab: SettingsV2TabTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,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;
|
||||||
|
|
||||||
@@ -165,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;
|
||||||
|
|
||||||
@@ -204,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,
|
||||||
@@ -229,7 +244,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
conflictResolutionPolicyProcedureBaseline: undefined,
|
conflictResolutionPolicyProcedureBaseline: undefined,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
|
|
||||||
initialNotification: undefined,
|
|
||||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -309,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 ||
|
||||||
@@ -320,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 ||
|
||||||
@@ -407,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: [],
|
||||||
@@ -418,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,
|
||||||
@@ -450,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 => {
|
||||||
@@ -540,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 });
|
||||||
|
|
||||||
@@ -693,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();
|
||||||
@@ -726,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,
|
||||||
@@ -856,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
|
||||||
@@ -877,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 =
|
||||||
@@ -915,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();
|
||||||
@@ -923,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,
|
||||||
@@ -1052,7 +1102,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
||||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
initialNotification: this.props.settingsTab.pendingNotification(),
|
|
||||||
throughputError: this.state.throughputError,
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1094,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,
|
||||||
@@ -1151,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({
|
||||||
@@ -1168,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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import ko from "knockout";
|
|
||||||
import React from "react";
|
|
||||||
import * as Constants from "../../../../Common/Constants";
|
import * as Constants from "../../../../Common/Constants";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
|
||||||
import { updateUserContext } from "../../../../UserContext";
|
import { updateUserContext } from "../../../../UserContext";
|
||||||
import Explorer from "../../../Explorer";
|
import Explorer from "../../../Explorer";
|
||||||
import { throughputUnit } from "../SettingsRenderUtils";
|
|
||||||
import { collection } from "../TestUtils";
|
import { collection } from "../TestUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
|
||||||
|
|
||||||
describe("ScaleComponent", () => {
|
describe("ScaleComponent", () => {
|
||||||
const targetThroughput = 6000;
|
|
||||||
|
|
||||||
const baseProps: ScaleComponentProps = {
|
const baseProps: ScaleComponentProps = {
|
||||||
collection: collection,
|
collection: collection,
|
||||||
database: undefined,
|
database: undefined,
|
||||||
@@ -36,39 +28,8 @@ describe("ScaleComponent", () => {
|
|||||||
onScaleDiscardableChange: () => {
|
onScaleDiscardableChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
initialNotification: {
|
|
||||||
description: `Throughput update for ${targetThroughput} ${throughputUnit}`,
|
|
||||||
} as DataModels.Notification,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders with correct initial notification", () => {
|
|
||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
|
||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
|
||||||
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(`${targetThroughput}`);
|
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
|
||||||
const maxThroughput = 5000;
|
|
||||||
newCollection.offer = ko.observable({
|
|
||||||
manualThroughput: undefined,
|
|
||||||
autoscaleMaxThroughput: maxThroughput,
|
|
||||||
minimumThroughput: 400,
|
|
||||||
id: "offer",
|
|
||||||
offerReplacePending: true,
|
|
||||||
});
|
|
||||||
const newProps = {
|
|
||||||
...baseProps,
|
|
||||||
initialNotification: undefined as DataModels.Notification,
|
|
||||||
collection: newCollection,
|
|
||||||
};
|
|
||||||
wrapper = shallow(<ScaleComponent {...newProps} />);
|
|
||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(`${maxThroughput}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
const scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
|||||||
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
getTextFieldStyles,
|
||||||
getThroughputApplyLongDelayMessage,
|
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
@@ -34,7 +33,6 @@ export interface ScaleComponentProps {
|
|||||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
initialNotification: DataModels.Notification;
|
|
||||||
throughputError?: string;
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,10 +100,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public getInitialNotificationElement = (): JSX.Element => {
|
public getInitialNotificationElement = (): JSX.Element => {
|
||||||
if (this.props.initialNotification) {
|
|
||||||
return this.getLongDelayMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.offer?.offerReplacePending) {
|
if (this.offer?.offerReplacePending) {
|
||||||
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
@@ -120,26 +114,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getLongDelayMessage = (): JSX.Element => {
|
|
||||||
const matches: string[] = this.props.initialNotification?.description.match(
|
|
||||||
`Throughput update for (.*) ${throughputUnit}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const throughput = this.props.throughputBaseline;
|
|
||||||
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
|
|
||||||
if (targetThroughput) {
|
|
||||||
return getThroughputApplyLongDelayMessage(
|
|
||||||
this.props.wasAutopilotOriginallySet,
|
|
||||||
throughput,
|
|
||||||
throughputUnit,
|
|
||||||
this.databaseId,
|
|
||||||
this.collectionId,
|
|
||||||
targetThroughput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={userContext?.databaseAccount}
|
databaseAccount={userContext?.databaseAccount}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|
||||||
<Stack
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledMessageBar
|
|
||||||
messageBarType={5}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
id="throughputApplyLongDelayMessage"
|
|
||||||
styles={
|
|
||||||
{
|
|
||||||
"root": {
|
|
||||||
"color": "windowtext",
|
|
||||||
"fontSize": 14,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
|
||||||
<br />
|
|
||||||
Database: test, Container: test
|
|
||||||
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
|
||||||
</Text>
|
|
||||||
</StyledMessageBar>
|
|
||||||
<Stack
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ThroughputInputAutoPilotV3Component
|
|
||||||
canExceedMaximumValue={true}
|
|
||||||
collectionName="test"
|
|
||||||
databaseName="test"
|
|
||||||
isAutoPilotSelected={false}
|
|
||||||
isEmulator={false}
|
|
||||||
isEnabled={true}
|
|
||||||
isFixed={false}
|
|
||||||
label="Throughput (6,000 - unlimited RU/s)"
|
|
||||||
maxAutoPilotThroughput={4000}
|
|
||||||
maxAutoPilotThroughputBaseline={4000}
|
|
||||||
maximum={1000000}
|
|
||||||
minimum={6000}
|
|
||||||
onAutoPilotSelected={[Function]}
|
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
|
||||||
onScaleDiscardableChange={[Function]}
|
|
||||||
onScaleSaveableChange={[Function]}
|
|
||||||
onThroughputChange={[Function]}
|
|
||||||
spendAckChecked={false}
|
|
||||||
throughput={1000}
|
|
||||||
throughputBaseline={1000}
|
|
||||||
usageSizeInKB={100}
|
|
||||||
wasAutopilotOriginallySet={true}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
`;
|
|
||||||
@@ -44,7 +44,6 @@ describe("SettingsUtils", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
getPendingThroughputSplitNotification: undefined,
|
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
};
|
};
|
||||||
newCollection.offer(undefined);
|
newCollection.offer(undefined);
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { useCallback } from "react";
|
|||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: (value?: React.RefObject<HTMLElement>) => void;
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
@@ -74,6 +74,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
openItems,
|
openItems,
|
||||||
}: TreeNodeComponentProps): JSX.Element => {
|
}: TreeNodeComponentProps): JSX.Element => {
|
||||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||||
|
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
|
||||||
const treeStyles = useTreeStyles();
|
const treeStyles = useTreeStyles();
|
||||||
|
|
||||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
||||||
@@ -141,7 +142,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
||||||
disabled={menuItem.isDisabled}
|
disabled={menuItem.isDisabled}
|
||||||
key={menuItem.label}
|
key={menuItem.label}
|
||||||
onClick={menuItem.onClick}
|
onClick={() => menuItem.onClick(contextMenuRef)}
|
||||||
>
|
>
|
||||||
{menuItem.label}
|
{menuItem.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -190,6 +191,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
||||||
data-test="TreeNode/ContextMenuTrigger"
|
data-test="TreeNode/ContextMenuTrigger"
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
|
ref={contextMenuRef}
|
||||||
icon={<MoreHorizontal20Regular />}
|
icon={<MoreHorizontal20Regular />}
|
||||||
/>
|
/>
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
|
|||||||
@@ -1478,14 +1478,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||||
onClick={[MockFunction enabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
enabledItem
|
enabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
onClick={[MockFunction disabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
disabledItem
|
disabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -1518,7 +1518,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||||
key="enabledItem"
|
key="enabledItem"
|
||||||
onClick={[MockFunction enabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
enabledItem
|
enabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -1526,7 +1526,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
key="disabledItem"
|
key="disabledItem"
|
||||||
onClick={[MockFunction disabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
disabledItem
|
disabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
|
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
@@ -9,7 +10,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
|
|||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -258,25 +259,8 @@ export default class Explorer {
|
|||||||
|
|
||||||
public async openLoginForEntraIDPopUp(): Promise<void> {
|
public async openLoginForEntraIDPopUp(): Promise<void> {
|
||||||
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
||||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
|
||||||
/\/$/,
|
|
||||||
"/.default",
|
|
||||||
);
|
|
||||||
const msalInstance = await getMsalInstance();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await msalInstance.loginPopup({
|
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false);
|
||||||
redirectUri: configContext.msalRedirectURI,
|
|
||||||
scopes: [],
|
|
||||||
});
|
|
||||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
|
||||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
|
||||||
msalInstance.setActiveAccount(cachedAccount);
|
|
||||||
const aadToken = await acquireTokenWithMsal(msalInstance, {
|
|
||||||
forceRefresh: true,
|
|
||||||
scopes: [hrefEndpoint],
|
|
||||||
authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`,
|
|
||||||
});
|
|
||||||
updateUserContext({ aadToken: aadToken });
|
updateUserContext({ aadToken: aadToken });
|
||||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1178,7 +1162,11 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
|
if (
|
||||||
|
userContext.apiType !== "SQL" ||
|
||||||
|
!userContext.subscriptionId ||
|
||||||
|
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const copilotEnabledPromise = getCopilotEnabled();
|
const copilotEnabledPromise = getCopilotEnabled();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { clear, collectionWasOpened, getItems, Type } from "Explorer/MostRecentActivity/MostRecentActivity";
|
||||||
import { observable } from "knockout";
|
import { observable } from "knockout";
|
||||||
import { mostRecentActivity } from "./MostRecentActivity";
|
|
||||||
|
|
||||||
describe("MostRecentActivity", () => {
|
describe("MostRecentActivity", () => {
|
||||||
const accountId = "some account";
|
const accountName = "some account";
|
||||||
|
|
||||||
beforeEach(() => mostRecentActivity.clear(accountId));
|
beforeEach(() => clear(accountName));
|
||||||
|
|
||||||
it("Has no items at first", () => {
|
it("Has no items at first", () => {
|
||||||
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
|
expect(getItems(accountName)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can record collections being opened", () => {
|
it("Can record collections being opened", () => {
|
||||||
@@ -18,9 +18,9 @@ describe("MostRecentActivity", () => {
|
|||||||
databaseId,
|
databaseId,
|
||||||
};
|
};
|
||||||
|
|
||||||
mostRecentActivity.collectionWasOpened(accountId, collection);
|
collectionWasOpened(accountName, collection);
|
||||||
|
|
||||||
const activity = mostRecentActivity.getItems(accountId);
|
const activity = getItems(accountName);
|
||||||
expect(activity).toEqual([
|
expect(activity).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
collectionId,
|
collectionId,
|
||||||
@@ -29,58 +29,24 @@ describe("MostRecentActivity", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can record notebooks being opened", () => {
|
it("Does not store duplicate entries", () => {
|
||||||
const name = "some notebook";
|
const collectionId = "some collection";
|
||||||
const path = "some path";
|
const databaseId = "some database";
|
||||||
const notebook = { name, path };
|
const collection = {
|
||||||
|
id: observable(collectionId),
|
||||||
|
databaseId,
|
||||||
|
};
|
||||||
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
collectionWasOpened(accountName, collection);
|
||||||
|
collectionWasOpened(accountName, collection);
|
||||||
|
|
||||||
const activity = mostRecentActivity.getItems(accountId);
|
const activity = getItems(accountName);
|
||||||
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
expect(activity).toEqual([
|
||||||
});
|
expect.objectContaining({
|
||||||
|
type: Type.OpenCollection,
|
||||||
it("Filters out duplicates", () => {
|
collectionId,
|
||||||
const name = "some notebook";
|
databaseId,
|
||||||
const path = "some path";
|
}),
|
||||||
const notebook = { name, path };
|
]);
|
||||||
const sameNotebook = { name, path };
|
|
||||||
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
|
|
||||||
|
|
||||||
const activity = mostRecentActivity.getItems(accountId);
|
|
||||||
expect(activity.length).toEqual(1);
|
|
||||||
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows for multiple accounts", () => {
|
|
||||||
const name = "some notebook";
|
|
||||||
const path = "some path";
|
|
||||||
const notebook = { name, path };
|
|
||||||
|
|
||||||
const anotherNotebook = { name: "Another " + name, path };
|
|
||||||
const anotherAccountId = "Another " + accountId;
|
|
||||||
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
|
||||||
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
|
|
||||||
|
|
||||||
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
|
|
||||||
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Can store multiple distinct elements, in FIFO order", () => {
|
|
||||||
const name = "some notebook";
|
|
||||||
const path = "some path";
|
|
||||||
const first = { name, path };
|
|
||||||
const second = { name: "Another " + name, path };
|
|
||||||
const third = { name, path: "Another " + path };
|
|
||||||
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, first);
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, second);
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, third);
|
|
||||||
|
|
||||||
const activity = mostRecentActivity.getItems(accountId);
|
|
||||||
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { AppStateComponentNames, deleteState, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
||||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||||
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
OpenCollection,
|
OpenCollection = "OpenCollection",
|
||||||
OpenNotebook,
|
OpenNotebook = "OpenNotebook",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenNotebookItem {
|
export interface OpenNotebookItem {
|
||||||
@@ -21,158 +21,174 @@ export interface OpenCollectionItem {
|
|||||||
|
|
||||||
type Item = OpenNotebookItem | OpenCollectionItem;
|
type Item = OpenNotebookItem | OpenCollectionItem;
|
||||||
|
|
||||||
// Update schemaVersion if you are going to change this interface
|
const itemsMaxNumber: number = 5;
|
||||||
interface StoredData {
|
|
||||||
schemaVersion: string;
|
|
||||||
itemsMap: { [accountId: string]: Item[] }; // FIFO
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores most recent activity
|
* Migrate old data to new AppState
|
||||||
*/
|
*/
|
||||||
class MostRecentActivity {
|
const migrateOldData = () => {
|
||||||
private static readonly schemaVersion: string = "2";
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
private static itemsMaxNumber: number = 5;
|
const oldDataSchemaVersion: string = "2";
|
||||||
private storedData: StoredData;
|
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
||||||
constructor() {
|
if (rawData) {
|
||||||
// Retrieve from local storage
|
const oldData = JSON.parse(rawData);
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
if (oldData.schemaVersion === oldDataSchemaVersion) {
|
||||||
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
const itemsMap: Record<string, Item[]> = oldData.itemsMap;
|
||||||
|
Object.keys(itemsMap).forEach((accountId: string) => {
|
||||||
if (!rawData) {
|
const accountName = accountId.split("/").pop();
|
||||||
this.storedData = MostRecentActivity.createEmptyData();
|
if (accountName) {
|
||||||
} else {
|
saveState(
|
||||||
try {
|
{
|
||||||
this.storedData = JSON.parse(rawData);
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
} catch (e) {
|
globalAccountName: accountName,
|
||||||
console.error("Unable to parse stored most recent activity. Use empty data:", rawData);
|
},
|
||||||
this.storedData = MostRecentActivity.createEmptyData();
|
itemsMap[accountId].map((item) => {
|
||||||
}
|
if ((item.type as unknown as number) === 0) {
|
||||||
|
item.type = Type.OpenCollection;
|
||||||
// If version doesn't match or schema broke, nuke it!
|
} else if ((item.type as unknown as number) === 1) {
|
||||||
if (
|
item.type = Type.OpenNotebook;
|
||||||
!this.storedData.hasOwnProperty("schemaVersion") ||
|
}
|
||||||
this.storedData["schemaVersion"] !== MostRecentActivity.schemaVersion
|
return item;
|
||||||
) {
|
}),
|
||||||
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
);
|
||||||
this.storedData = MostRecentActivity.createEmptyData();
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.storedData = MostRecentActivity.createEmptyData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let p in this.storedData.itemsMap) {
|
// Remove old data
|
||||||
this.cleanupItems(p);
|
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addItem = (accountName: string, newItem: Item): void => {
|
||||||
|
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
||||||
|
// if (!accountId) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
let items =
|
||||||
|
(loadState({
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
}) as Item[]) || [];
|
||||||
|
|
||||||
|
// Remove duplicate
|
||||||
|
items = removeDuplicate(newItem, items);
|
||||||
|
|
||||||
|
items.unshift(newItem);
|
||||||
|
items = cleanupItems(items, accountName);
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
},
|
||||||
|
items,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItems = (accountName: string): Item[] => {
|
||||||
|
if (!accountName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(loadState({
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
}) as Item[]) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const collectionWasOpened = (
|
||||||
|
accountName: string,
|
||||||
|
{ id, databaseId }: Pick<CollectionBase, "id" | "databaseId">,
|
||||||
|
) => {
|
||||||
|
if (accountName === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionId = id();
|
||||||
|
addItem(accountName, {
|
||||||
|
type: Type.OpenCollection,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clear = (accountName: string): void => {
|
||||||
|
if (!accountName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteState({
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort object by key
|
||||||
|
const sortObjectKeys = (unordered: Record<string, unknown>): Record<string, unknown> => {
|
||||||
|
return Object.keys(unordered)
|
||||||
|
.sort()
|
||||||
|
.reduce((obj: Record<string, unknown>, key: string) => {
|
||||||
|
obj[key] = unordered[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find items by doing strict comparison and remove from array if duplicate is found.
|
||||||
|
* Modifies the array.
|
||||||
|
* @param item
|
||||||
|
* @param itemsArray
|
||||||
|
* @returns new array
|
||||||
|
*/
|
||||||
|
const removeDuplicate = (item: Item, itemsArray: Item[]): Item[] => {
|
||||||
|
if (!itemsArray) {
|
||||||
|
return itemsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Item[] = [...itemsArray];
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
const currentItem = result[i];
|
||||||
|
|
||||||
|
if (
|
||||||
|
JSON.stringify(sortObjectKeys(currentItem as unknown as Record<string, unknown>)) ===
|
||||||
|
JSON.stringify(sortObjectKeys(item as unknown as Record<string, unknown>))
|
||||||
|
) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.saveToLocalStorage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createEmptyData(): StoredData {
|
if (index !== -1) {
|
||||||
return {
|
result.splice(index, 1);
|
||||||
schemaVersion: MostRecentActivity.schemaVersion,
|
|
||||||
itemsMap: {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static isEmpty(object: any) {
|
return result;
|
||||||
return Object.keys(object).length === 0 && object.constructor === Object;
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove unknown types
|
||||||
|
* Limit items to max number
|
||||||
|
* Modifies the array.
|
||||||
|
*/
|
||||||
|
const cleanupItems = (items: Item[], accountName: string): Item[] => {
|
||||||
|
if (accountName === undefined) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveToLocalStorage() {
|
const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber);
|
||||||
if (MostRecentActivity.isEmpty(this.storedData.itemsMap)) {
|
if (itemsArray.length === 0) {
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
deleteState({
|
||||||
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
}
|
globalAccountName: accountName,
|
||||||
// Don't save if empty
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
|
|
||||||
}
|
|
||||||
|
|
||||||
private addItem(accountId: string, newItem: Item): void {
|
|
||||||
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
|
||||||
// if (!accountId) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Remove duplicate
|
|
||||||
MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]);
|
|
||||||
|
|
||||||
this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || [];
|
|
||||||
this.storedData.itemsMap[accountId].unshift(newItem);
|
|
||||||
this.cleanupItems(accountId);
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getItems(accountId: string): Item[] {
|
|
||||||
return this.storedData.itemsMap[accountId] || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
|
|
||||||
const collectionId = id();
|
|
||||||
this.addItem(accountId, {
|
|
||||||
type: Type.OpenCollection,
|
|
||||||
databaseId,
|
|
||||||
collectionId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return itemsArray;
|
||||||
|
};
|
||||||
|
|
||||||
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
|
migrateOldData();
|
||||||
this.addItem(accountId, {
|
|
||||||
type: Type.OpenNotebook,
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(accountId: string): void {
|
|
||||||
delete this.storedData.itemsMap[accountId];
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find items by doing strict comparison and remove from array if duplicate is found
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
private static removeDuplicate(item: Item, itemsArray: Item[]): void {
|
|
||||||
if (!itemsArray) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = -1;
|
|
||||||
for (let i = 0; i < itemsArray.length; i++) {
|
|
||||||
const currentItem = itemsArray[i];
|
|
||||||
if (JSON.stringify(currentItem) === JSON.stringify(item)) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
itemsArray.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove unknown types
|
|
||||||
* Limit items to max number
|
|
||||||
*/
|
|
||||||
private cleanupItems(accountId: string): void {
|
|
||||||
if (!this.storedData.itemsMap.hasOwnProperty(accountId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsArray = this.storedData.itemsMap[accountId]
|
|
||||||
.filter((item) => item.type in Type)
|
|
||||||
.slice(0, MostRecentActivity.itemsMaxNumber);
|
|
||||||
if (itemsArray.length === 0) {
|
|
||||||
delete this.storedData.itemsMap[accountId];
|
|
||||||
} else {
|
|
||||||
this.storedData.itemsMap[accountId] = itemsArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mostRecentActivity = new MostRecentActivity();
|
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
|
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 { SubscriptionType } from "Contracts/SubscriptionType";
|
import {
|
||||||
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
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> {
|
||||||
@@ -125,7 +137,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
createNewDatabase:
|
createNewDatabase:
|
||||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
isSharedThroughputChecked: getNewDatabaseSharedThroughputDefault(),
|
||||||
selectedDatabaseId:
|
selectedDatabaseId:
|
||||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,9 +905,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 +921,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"
|
||||||
@@ -1138,10 +1181,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSharedThroughputDefault(): boolean {
|
|
||||||
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFreeTierIndexingText(): string {
|
private getFreeTierIndexingText(): string {
|
||||||
return this.state.enableIndexing
|
return this.state.enableIndexing
|
||||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||||
@@ -1215,6 +1254,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;
|
||||||
@@ -1278,6 +1330,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;
|
||||||
@@ -1334,9 +1390,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
if (!this.state.vectorPolicyValidated) {
|
||||||
return false;
|
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.fullTextPolicyValidated) {
|
||||||
|
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1427,6 +1490,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,
|
||||||
@@ -1486,6 +1553,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 });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||||
|
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
@@ -48,7 +49,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
|
|
||||||
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
||||||
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
||||||
subscriptionType !== SubscriptionType.EA && !isServerlessAccount(),
|
getNewDatabaseSharedThroughputDefault(),
|
||||||
);
|
);
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
const [formErrors, setFormErrors] = useState<string>("");
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={true}
|
checked={false}
|
||||||
label="Provision throughput"
|
label="Provision throughput"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
@@ -90,14 +90,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ThroughputInput
|
|
||||||
isDatabase={true}
|
|
||||||
isSharded={true}
|
|
||||||
onCostAcknowledgeChange={[Function]}
|
|
||||||
setIsAutoscale={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
setThroughputValue={[Function]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
message:
|
message:
|
||||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
};
|
};
|
||||||
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
|
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`;
|
||||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
@@ -132,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
<div className="confirmDeleteInput">
|
<div className="confirmDeleteInput">
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
|
<Text variant="small">{confirmDatabase}</Text>
|
||||||
<TextField
|
<TextField
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
AuthError as msalAuthError,
|
||||||
|
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
|
||||||
|
} from "@azure/msal-browser";
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ChoiceGroup,
|
ChoiceGroup,
|
||||||
@@ -5,8 +9,6 @@ import {
|
|||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
ISpinButtonStyles,
|
ISpinButtonStyles,
|
||||||
IToggleStyles,
|
IToggleStyles,
|
||||||
MessageBar,
|
|
||||||
MessageBarType,
|
|
||||||
Position,
|
Position,
|
||||||
SpinButton,
|
SpinButton,
|
||||||
Toggle,
|
Toggle,
|
||||||
@@ -30,6 +32,7 @@ import {
|
|||||||
} from "Shared/StorageUtility";
|
} from "Shared/StorageUtility";
|
||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
@@ -108,7 +111,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
||||||
: Constants.RBACOptions.setAutomaticRBACOption,
|
: Constants.RBACOptions.setAutomaticRBACOption,
|
||||||
);
|
);
|
||||||
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||||
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||||
@@ -203,6 +205,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
hasDataPlaneRbacSettingChanged: true,
|
hasDataPlaneRbacSettingChanged: true,
|
||||||
});
|
});
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
|
try {
|
||||||
|
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
|
||||||
|
updateUserContext({ aadToken: aadToken });
|
||||||
|
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||||
|
} catch (authError) {
|
||||||
|
if (
|
||||||
|
authError instanceof msalAuthError &&
|
||||||
|
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
|
||||||
|
) {
|
||||||
|
logConsoleError(
|
||||||
|
`We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logConsoleError(
|
||||||
|
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: false,
|
dataPlaneRbacEnabled: false,
|
||||||
@@ -347,13 +367,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
option: IChoiceGroupOption,
|
option: IChoiceGroupOption,
|
||||||
): void => {
|
): void => {
|
||||||
setEnableDataPlaneRBACOption(option.key);
|
setEnableDataPlaneRBACOption(option.key);
|
||||||
|
|
||||||
const shouldShowWarning =
|
|
||||||
(option.key === Constants.RBACOptions.setTrueRBACOption ||
|
|
||||||
(option.key === Constants.RBACOptions.setAutomaticRBACOption &&
|
|
||||||
userContext.databaseAccount.properties.disableLocalAuth === true)) &&
|
|
||||||
!useDataPlaneRbac.getState().aadTokenUpdated;
|
|
||||||
setShowDataPlaneRBACWarning(shouldShowWarning);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
@@ -528,17 +541,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
|
||||||
<MessageBar
|
|
||||||
messageBarType={MessageBarType.warning}
|
|
||||||
isMultiline={true}
|
|
||||||
onDismiss={() => setShowDataPlaneRBACWarning(false)}
|
|
||||||
dismissButtonAriaLabel="Close"
|
|
||||||
>
|
|
||||||
Please click on "Login for Entra ID RBAC" button prior to performing Entra ID RBAC
|
|
||||||
operations
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className={styles.settingsSectionDescription}>
|
||||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||||
ID RBAC.
|
ID RBAC.
|
||||||
@@ -608,16 +610,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
<AccordionItem value="4">
|
<AccordionItem value="4">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>RU Threshold</div>
|
<div className={styles.header}>RU Limit</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className={styles.settingsSectionDescription}>
|
||||||
If a query exceeds a configured RU threshold, the query will be aborted.
|
If a query exceeds a configured RU limit, the query will be aborted.
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
styles={toggleStyles}
|
styles={toggleStyles}
|
||||||
label="Enable RU threshold"
|
label="Enable RU limit"
|
||||||
onChange={handleOnRUThresholdToggleChange}
|
onChange={handleOnRUThresholdToggleChange}
|
||||||
defaultChecked={ruThresholdEnabled}
|
defaultChecked={ruThresholdEnabled}
|
||||||
/>
|
/>
|
||||||
@@ -625,7 +627,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
{ruThresholdEnabled && (
|
{ruThresholdEnabled && (
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
label="RU Threshold (RU)"
|
label="RU Limit (RU)"
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
>
|
||||||
RU Threshold
|
RU Limit
|
||||||
</div>
|
</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
@@ -164,11 +164,11 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
>
|
>
|
||||||
If a query exceeds a configured RU threshold, the query will be aborted.
|
If a query exceeds a configured RU limit, the query will be aborted.
|
||||||
</div>
|
</div>
|
||||||
<StyledToggleBase
|
<StyledToggleBase
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
label="Enable RU threshold"
|
label="Enable RU limit"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -193,7 +193,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
defaultValue="5000"
|
defaultValue="5000"
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
label="RU Threshold (RU)"
|
label="RU Limit (RU)"
|
||||||
labelPosition={0}
|
labelPosition={0}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
|||||||
@@ -61,7 +61,15 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
|||||||
if (checked) {
|
if (checked) {
|
||||||
selectedColumnIdsSet.add(id);
|
selectedColumnIdsSet.add(id);
|
||||||
} else {
|
} else {
|
||||||
if (selectedColumnIdsSet.size === 1 && selectedColumnIdsSet.has(id)) {
|
/* selectedColumnIds may contain ids that are not in columnDefinitions, because the selected
|
||||||
|
* ids may have been loaded from persistence, but don't exist in the current retrieved documents.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.from(selectedColumnIdsSet).filter((id) => columnDefinitions.find((def) => def.id === id) !== undefined)
|
||||||
|
.length === 1 &&
|
||||||
|
selectedColumnIdsSet.has(id)
|
||||||
|
) {
|
||||||
// Don't allow unchecking the last column
|
// Don't allow unchecking the last column
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -106,7 +106,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={true}
|
checked={false}
|
||||||
label="Share throughput across containers"
|
label="Share throughput across containers"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
@@ -137,14 +137,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ThroughputInput
|
|
||||||
isDatabase={true}
|
|
||||||
isSharded={true}
|
|
||||||
onCostAcknowledgeChange={[Function]}
|
|
||||||
setIsAutoscale={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
setThroughputValue={[Function]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Separator
|
<Separator
|
||||||
className="panelSeparator"
|
className="panelSeparator"
|
||||||
@@ -263,6 +255,14 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</CustomizedDefaultButton>
|
</CustomizedDefaultButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<ThroughputInput
|
||||||
|
isDatabase={false}
|
||||||
|
isSharded={true}
|
||||||
|
onCostAcknowledgeChange={[Function]}
|
||||||
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
|
setThroughputValue={[Function]}
|
||||||
|
/>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
|||||||
@@ -361,13 +361,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="css-113"
|
className="css-113"
|
||||||
>
|
>
|
||||||
Confirm by typing the
|
Confirm by typing the Database id (name)
|
||||||
Database
|
|
||||||
id
|
|
||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Confirm by typing the Database id"
|
ariaLabel="Confirm by typing the Database id (name)"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
@@ -382,7 +380,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
ariaLabel="Confirm by typing the Database id"
|
ariaLabel="Confirm by typing the Database id (name)"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
@@ -677,7 +675,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
aria-label="Confirm by typing the Database id"
|
aria-label="Confirm by typing the Database id (name)"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="ms-TextField-field field-117"
|
className="ms-TextField-field field-117"
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
|
|||||||
the query builder.
|
the query builder.
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
||||||
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
|
<Text style={{ fontSize: 13 }}>CopilotSampleDB</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
||||||
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
||||||
|
|||||||
@@ -79,9 +79,13 @@ export const QueryCopilotFeedbackModal = ({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to
|
||||||
|
improve your and your organization’s experience with this product. If you have any questions about the use
|
||||||
|
of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the
|
||||||
|
Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the
|
||||||
|
feedback you submit is considered Personal Data under that addendum. Please see the{" "}
|
||||||
{
|
{
|
||||||
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
<Link href="https://go.microsoft.com/fwlink/?LinkId=521839" target="_blank">
|
||||||
Privacy statement
|
Privacy statement
|
||||||
</Link>
|
</Link>
|
||||||
}{" "}
|
}{" "}
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -236,10 +236,10 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -373,10 +373,10 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -510,10 +510,10 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -647,10 +647,10 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -784,10 +784,10 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -936,10 +936,10 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://privacy.microsoft.com/privacystatement"
|
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import {
|
|||||||
SuggestedPrompt,
|
SuggestedPrompt,
|
||||||
getSampleDatabaseSuggestedPrompts,
|
getSampleDatabaseSuggestedPrompts,
|
||||||
getSuggestedPrompts,
|
getSuggestedPrompts,
|
||||||
|
readPromptHistory,
|
||||||
|
savePromptHistory,
|
||||||
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
@@ -136,9 +138,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount));
|
||||||
const cachedHistories = cachedHistoriesString?.split("|");
|
|
||||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
|
||||||
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
||||||
? getSampleDatabaseSuggestedPrompts()
|
? getSampleDatabaseSuggestedPrompts()
|
||||||
: getSuggestedPrompts();
|
: getSuggestedPrompts();
|
||||||
@@ -172,7 +172,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
setHistories(newHistories);
|
setHistories(newHistories);
|
||||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
savePromptHistory(userContext.databaseAccount, newHistories);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetMessageStates = (): void => {
|
const resetMessageStates = (): void => {
|
||||||
|
|||||||
@@ -1,10 +1,39 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
|
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
||||||
|
import { updateUserContext } from "UserContext";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { QueryCopilotTab } from "./QueryCopilotTab";
|
import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||||
|
|
||||||
describe("Query copilot tab snapshot test", () => {
|
describe("Query copilot tab snapshot test", () => {
|
||||||
it("should render with initial input", () => {
|
it("should render with initial input", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "name",
|
||||||
|
properties: undefined,
|
||||||
|
id: "",
|
||||||
|
location: "",
|
||||||
|
type: "",
|
||||||
|
kind: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadState = (path: StorePath) => {
|
||||||
|
if (
|
||||||
|
path.componentName === AppStateComponentNames.QueryCopilot &&
|
||||||
|
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
||||||
|
) {
|
||||||
|
return { enabled: true };
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||||
|
loadState,
|
||||||
|
}));
|
||||||
|
|
||||||
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
|||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
@@ -18,18 +19,13 @@ import SplitterLayout from "react-splitter-layout";
|
|||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as StringUtility from "../../Shared/StringUtility";
|
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
||||||
|
|
||||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
readCopilotToggleStatus(userContext.databaseAccount),
|
||||||
);
|
);
|
||||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
|
||||||
: true;
|
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
|
||||||
const [tabActive, setTabActive] = useState<boolean>(true);
|
const [tabActive, setTabActive] = useState<boolean>(true);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
@@ -88,7 +84,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString());
|
saveCopilotToggleStatus(userContext.databaseAccount, toggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
|
|
||||||
// Mock the items.query method to return the mockResult
|
// Mock the items.query method to return the mockResult
|
||||||
(
|
(
|
||||||
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").items.query as jest.Mock
|
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").items.query as jest.Mock
|
||||||
).mockReturnValue(mockResult);
|
).mockReturnValue(mockResult);
|
||||||
|
|
||||||
const result = querySampleDocuments(query, options);
|
const result = querySampleDocuments(query, options);
|
||||||
@@ -119,10 +119,10 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
const result = await readSampleDocument(documentId);
|
const result = await readSampleDocument(documentId);
|
||||||
|
|
||||||
expect(sampleDataClient).toHaveBeenCalled();
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
|
||||||
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
expect(
|
expect(
|
||||||
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read,
|
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
expect(result).toEqual(expectedResponse);
|
expect(result).toEqual(expectedResponse);
|
||||||
});
|
});
|
||||||
@@ -144,10 +144,10 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
||||||
|
|
||||||
expect(sampleDataClient).toHaveBeenCalled();
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
|
||||||
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
expect(
|
expect(
|
||||||
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read,
|
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { handleError } from "Common/ErrorHandlingUtils";
|
|||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
|
import { DatabaseAccount } from "Contracts/DataModels";
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
|
import { AppStateComponentNames, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
import * as StringUtility from "../../Shared/StringUtility";
|
||||||
|
|
||||||
export interface SuggestedPrompt {
|
export interface SuggestedPrompt {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -54,3 +57,110 @@ export const getSuggestedPrompts = (): SuggestedPrompt[] => {
|
|||||||
{ id: 3, text: "Find the oldest item added to my collection" },
|
{ id: 3, text: "Find the oldest item added to my collection" },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prompt history persistence
|
||||||
|
export enum CopilotSubComponentNames {
|
||||||
|
promptHistory = "PromptHistory",
|
||||||
|
toggleStatus = "ToggleStatus",
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLegacyHistoryKey = (databaseAccount: DatabaseAccount): string =>
|
||||||
|
`${databaseAccount?.id}-queryCopilotHistories`;
|
||||||
|
const getLegacyToggleStatusKey = (databaseAccount: DatabaseAccount): string =>
|
||||||
|
`${databaseAccount?.id}-queryCopilotToggleStatus`;
|
||||||
|
|
||||||
|
// Migration only needs to run once
|
||||||
|
let hasMigrated = false;
|
||||||
|
// Migrate old prompt history to new format
|
||||||
|
export const migrateCopilotPersistence = (databaseAccount: DatabaseAccount): void => {
|
||||||
|
if (hasMigrated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = getLegacyHistoryKey(databaseAccount);
|
||||||
|
let item = localStorage.getItem(key);
|
||||||
|
if (item !== undefined && item !== null) {
|
||||||
|
const historyItems = item.split("|");
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.promptHistory,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
historyItems,
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = getLegacyToggleStatusKey(databaseAccount);
|
||||||
|
item = localStorage.getItem(key);
|
||||||
|
if (item !== undefined && item !== null) {
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.toggleStatus,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
StringUtility.toBoolean(item),
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMigrated = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readPromptHistory = (databaseAccount: DatabaseAccount): string[] => {
|
||||||
|
migrateCopilotPersistence(databaseAccount);
|
||||||
|
return (
|
||||||
|
(loadState({
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.promptHistory,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
}) as string[]) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePromptHistory = (databaseAccount: DatabaseAccount, historyItems: string[]): void => {
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.promptHistory,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
historyItems,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readCopilotToggleStatus = (databaseAccount: DatabaseAccount): boolean => {
|
||||||
|
migrateCopilotPersistence(databaseAccount);
|
||||||
|
return !!loadState({
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.toggleStatus,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
}) as boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveCopilotToggleStatus = (databaseAccount: DatabaseAccount, status: boolean): void => {
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.toggleStatus,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -36,7 +36,6 @@ import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import * as StringUtility from "../../../Shared/StringUtility";
|
|
||||||
|
|
||||||
async function fetchWithTimeout(
|
async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -361,9 +360,7 @@ export const QueryDocumentsPerPage = async (
|
|||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = StringUtility.toBoolean(
|
const isCopilotActive = readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
|
||||||
);
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
|||||||
@@ -17,38 +17,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<QueryCopilotPromptbar
|
|
||||||
containerId="SampleContainer"
|
|
||||||
databaseId="CopilotSampleDb"
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleCopilot={[Function]}
|
|
||||||
/>
|
|
||||||
<Stack
|
<Stack
|
||||||
className="tabPaneContentContainer"
|
className="tabPaneContentContainer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -282,67 +282,69 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
<div className="sidebarContainer">
|
||||||
{/* Collections Tree - Start */}
|
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
||||||
{hasSidebar && (
|
{/* Collections Tree - Start */}
|
||||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
{hasSidebar && (
|
||||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||||
<div className={styles.sidebarContainer}>
|
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||||
{loading && (
|
<div className={styles.sidebarContainer}>
|
||||||
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
{loading && (
|
||||||
// https://github.com/microsoft/fluentui/issues/29076
|
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
||||||
<div className={styles.loadingProgressBar} title="Refreshing tree..." />
|
// https://github.com/microsoft/fluentui/issues/29076
|
||||||
)}
|
<div className={styles.loadingProgressBar} title="Refreshing tree..." />
|
||||||
{expanded ? (
|
)}
|
||||||
<>
|
{expanded ? (
|
||||||
<div className={styles.floatingControlsContainer}>
|
<>
|
||||||
<div className={styles.floatingControls}>
|
<div className={styles.floatingControlsContainer}>
|
||||||
<button
|
<div className={styles.floatingControls}>
|
||||||
type="button"
|
<button
|
||||||
data-test="Sidebar/RefreshButton"
|
type="button"
|
||||||
className={styles.floatingControlButton}
|
data-test="Sidebar/RefreshButton"
|
||||||
disabled={loading}
|
className={styles.floatingControlButton}
|
||||||
title="Refresh"
|
disabled={loading}
|
||||||
onClick={onRefreshClick}
|
title="Refresh"
|
||||||
>
|
onClick={onRefreshClick}
|
||||||
<ArrowSync12Regular />
|
>
|
||||||
</button>
|
<ArrowSync12Regular />
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
className={styles.floatingControlButton}
|
type="button"
|
||||||
title="Collapse sidebar"
|
className={styles.floatingControlButton}
|
||||||
onClick={() => collapse()}
|
title="Collapse sidebar"
|
||||||
>
|
onClick={() => collapse()}
|
||||||
<ChevronLeft12Regular />
|
>
|
||||||
</button>
|
<ChevronLeft12Regular />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
className={styles.expandedContent}
|
||||||
className={styles.expandedContent}
|
style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}
|
||||||
style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}
|
>
|
||||||
|
{hasGlobalCommands && <GlobalCommands explorer={explorer} />}
|
||||||
|
<ResourceTree explorer={explorer} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.floatingControlButton}
|
||||||
|
title="Expand sidebar"
|
||||||
|
onClick={() => expand()}
|
||||||
>
|
>
|
||||||
{hasGlobalCommands && <GlobalCommands explorer={explorer} />}
|
<ChevronRight12Regular />
|
||||||
<ResourceTree explorer={explorer} />
|
</button>
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
) : (
|
</CosmosFluentProvider>
|
||||||
<button
|
</Allotment.Pane>
|
||||||
type="button"
|
)}
|
||||||
className={styles.floatingControlButton}
|
<Allotment.Pane minSize={200}>
|
||||||
title="Expand sidebar"
|
<Tabs explorer={explorer} />
|
||||||
onClick={() => expand()}
|
|
||||||
>
|
|
||||||
<ChevronRight12Regular />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CosmosFluentProvider>
|
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
)}
|
</Allotment>
|
||||||
<Allotment.Pane minSize={200}>
|
</div>
|
||||||
<Tabs explorer={explorer} />
|
|
||||||
</Allotment.Pane>
|
|
||||||
</Allotment>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private clearMostRecent = (): void => {
|
private clearMostRecent = (): void => {
|
||||||
MostRecentActivity.mostRecentActivity.clear(userContext.databaseAccount?.id);
|
MostRecentActivity.clear(userContext.databaseAccount?.name);
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createRecentItems(): SplashScreenItem[] {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
|
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
default: {
|
default: {
|
||||||
const unknownActivity: never = activity;
|
const unknownActivity: never = activity;
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const htmlAttributeNames = {
|
|||||||
dataTableContentTypeAttr: "contentType_attr",
|
dataTableContentTypeAttr: "contentType_attr",
|
||||||
dataTableSnapshotAttr: "snapshot_attr",
|
dataTableSnapshotAttr: "snapshot_attr",
|
||||||
dataTableRowKeyAttr: "rowKey_attr",
|
dataTableRowKeyAttr: "rowKey_attr",
|
||||||
|
dataTablePartitionKeyAttr: "partKey_attr",
|
||||||
dataTableMessageIdAttr: "messageId_attr",
|
dataTableMessageIdAttr: "messageId_attr",
|
||||||
dataTableHeaderIndex: "data-column-index",
|
dataTableHeaderIndex: "data-column-index",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: an
|
|||||||
* from UI elements.
|
* from UI elements.
|
||||||
*/
|
*/
|
||||||
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
||||||
|
if (aData.PartitionKey && aData.PartitionKey._) {
|
||||||
|
$(nRow).attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr, aData.PartitionKey._);
|
||||||
|
}
|
||||||
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
||||||
return nRow;
|
return nRow;
|
||||||
}
|
}
|
||||||
@@ -205,6 +208,10 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi
|
|||||||
selected &&
|
selected &&
|
||||||
selected.forEach((b: Entities.ITableEntity) => {
|
selected.forEach((b: Entities.ITableEntity) => {
|
||||||
var sel = DataTableOperations.getRowSelector([
|
var sel = DataTableOperations.getRowSelector([
|
||||||
|
{
|
||||||
|
key: Constants.htmlAttributeNames.dataTablePartitionKeyAttr,
|
||||||
|
value: b.PartitionKey && b.PartitionKey._ && b.PartitionKey._.toString(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
||||||
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
||||||
@@ -370,8 +377,9 @@ function updateSelectionStatus(oSettings: any): void {
|
|||||||
for (var i = 0; i < $dataTableRows.length; i++) {
|
for (var i = 0; i < $dataTableRows.length; i++) {
|
||||||
var $row: JQuery = $dataTableRows.eq(i);
|
var $row: JQuery = $dataTableRows.eq(i);
|
||||||
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
||||||
|
var partitionKey: string = $row.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr);
|
||||||
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
||||||
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
|
if (table.isItemSelected(table.getTableEntityKeys(rowKey, partitionKey))) {
|
||||||
$row.attr("tabindex", "0");
|
$row.attr("tabindex", "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ export default class DataTableOperationManager {
|
|||||||
// Simply select the first item in this case.
|
// Simply select the first item in this case.
|
||||||
var lastSelectedItemIndex = lastSelectedItem
|
var lastSelectedItemIndex = lastSelectedItem
|
||||||
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._),
|
this._tableEntityListViewModel.getTableEntityKeys(
|
||||||
|
lastSelectedItem.RowKey._,
|
||||||
|
lastSelectedItem.PartitionKey && lastSelectedItem.PartitionKey._,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: -1;
|
: -1;
|
||||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
||||||
@@ -147,13 +150,14 @@ export default class DataTableOperationManager {
|
|||||||
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
|
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
|
||||||
return {
|
return {
|
||||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
||||||
|
PartitionKey: $elem.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
this._tableEntityListViewModel.lastSelectedItem = entity;
|
||||||
@@ -168,7 +172,7 @@ export default class DataTableOperationManager {
|
|||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +194,11 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Adding item not previously in selection
|
// Adding item not previously in selection
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
||||||
} else {
|
} else {
|
||||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
@@ -212,10 +216,10 @@ export default class DataTableOperationManager {
|
|||||||
if (anchorItem) {
|
if (anchorItem) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._),
|
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.PartitionKey._, anchorItem.RowKey._),
|
||||||
);
|
);
|
||||||
|
|
||||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
var startIndex = Math.min(elementIndex, anchorIndex);
|
||||||
@@ -234,24 +238,25 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (this._tableEntityListViewModel.selected().length) {
|
if (this._tableEntityListViewModel.selected().length) {
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
}
|
}
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToSelection(rowKey: string) {
|
private addToSelection(rowKey: string, partitionKey?: string) {
|
||||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(rowKey, partitionKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedEntity != null) {
|
if (selectedEntity != null) {
|
||||||
this._tableEntityListViewModel.selected.push(selectedEntity);
|
this._tableEntityListViewModel.selected.push(selectedEntity);
|
||||||
}
|
}
|
||||||
|
console.log(this._tableEntityListViewModel.selected().length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selecting first row if the selection is empty.
|
// Selecting first row if the selection is empty.
|
||||||
@@ -269,7 +274,7 @@ export default class DataTableOperationManager {
|
|||||||
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
||||||
this._tableEntityListViewModel.clearLastSelected();
|
this._tableEntityListViewModel.clearLastSelected();
|
||||||
|
|
||||||
this.addToSelection(firstEntity.RowKey._);
|
this.addToSelection(firstEntity.RowKey._, firstEntity.PartitionKey && firstEntity.PartitionKey._);
|
||||||
|
|
||||||
// Update last selection
|
// Update last selection
|
||||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
||||||
|
|||||||
@@ -128,8 +128,14 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTableEntityKeys(rowKey: string): Entities.IProperty[] {
|
public getTableEntityKeys(rowKey: string, partitionKey: string): Entities.IProperty[] {
|
||||||
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
const properties: Entities.IProperty[] = [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
||||||
|
|
||||||
|
if (partitionKey) {
|
||||||
|
properties.push({ key: Constants.EntityKeyNames.PartitionKey, value: partitionKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
|
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
|
||||||
@@ -261,7 +267,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
}
|
}
|
||||||
var oldEntityIndex: number = _.findIndex(
|
var oldEntityIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._,
|
(data: Entities.ITableEntity) =>
|
||||||
|
data.RowKey._ === entity.RowKey._ && data.PartitionKey._ === entity.PartitionKey._,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cache.data.splice(oldEntityIndex, 1, entity);
|
this.cache.data.splice(oldEntityIndex, 1, entity);
|
||||||
@@ -285,7 +292,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
entities.forEach((entity: Entities.ITableEntity) => {
|
entities.forEach((entity: Entities.ITableEntity) => {
|
||||||
var cachedIndex: number = _.findIndex(
|
var cachedIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._,
|
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._ && e.PartitionKey._ === entity.PartitionKey._,
|
||||||
);
|
);
|
||||||
if (cachedIndex >= 0) {
|
if (cachedIndex >= 0) {
|
||||||
this.cache.data.splice(cachedIndex, 1);
|
this.cache.data.splice(cachedIndex, 1);
|
||||||
@@ -393,6 +400,16 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override as Tables can have the same Row key in different Partition keys
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
||||||
|
return _.find(this.items(), (item: Entities.ITableEntity) => {
|
||||||
|
return this.matchesKeys(item, itemKeys);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private prefetchAndRender(
|
private prefetchAndRender(
|
||||||
tableQuery: Entities.ITableQuery,
|
tableQuery: Entities.ITableQuery,
|
||||||
tablePageStartIndex: number,
|
tablePageStartIndex: number,
|
||||||
|
|||||||
@@ -36,4 +36,5 @@ export interface ITableQuery {
|
|||||||
|
|
||||||
export interface ITableEntityIdentity {
|
export interface ITableEntityIdentity {
|
||||||
RowKey: string;
|
RowKey: string;
|
||||||
|
PartitionKey?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -757,6 +757,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
CassandraProxyEndpoints.Mooncake,
|
CassandraProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
// Definitions of State data
|
// Definitions of State data
|
||||||
|
|
||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
deleteState,
|
||||||
|
loadState,
|
||||||
|
saveState,
|
||||||
|
saveStateDebounced,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
const componentName = "DocumentsTab";
|
const componentName = AppStateComponentNames.DocumentsTab;
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
FilterHistory = "FilterHistory",
|
FilterHistory = "FilterHistory",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
||||||
|
import { waitFor } from "@testing-library/react";
|
||||||
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import {
|
import {
|
||||||
ButtonsDependencies,
|
ButtonsDependencies,
|
||||||
@@ -46,6 +49,7 @@ jest.mock("Common/dataAccess/queryDocuments", () => ({
|
|||||||
requestCharge: 1,
|
requestCharge: 1,
|
||||||
activityId: "activityId",
|
activityId: "activityId",
|
||||||
indexMetrics: "indexMetrics",
|
indexMetrics: "indexMetrics",
|
||||||
|
correlatedActivityId: undefined,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
@@ -65,12 +69,14 @@ jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
|||||||
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockDialogState = {
|
||||||
|
showOkCancelModalDialog: jest.fn((title: string, subText: string, okLabel: string, onOk: () => void) => onOk()),
|
||||||
|
showOkModalDialog: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Dialog", () => ({
|
jest.mock("Explorer/Controls/Dialog", () => ({
|
||||||
useDialog: {
|
useDialog: {
|
||||||
getState: jest.fn(() => ({
|
getState: jest.fn(() => mockDialogState),
|
||||||
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
|
|
||||||
showOkModalDialog: () => {},
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -80,6 +86,10 @@ jest.mock("Common/dataAccess/deleteDocument", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/ProgressModalDialog", () => ({
|
||||||
|
ProgressModalDialog: 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 () => {
|
||||||
@@ -376,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", () => {
|
||||||
@@ -469,7 +463,29 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document asks for confirmation", () => {
|
it("clicking Delete Document asks for confirmation", async () => {
|
||||||
|
act(async () => {
|
||||||
|
await useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking Delete Document for NoSql shows progress dialog", () => {
|
||||||
|
act(() => {
|
||||||
|
useCommandBar
|
||||||
|
.getState()
|
||||||
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
|
.onCommandClick(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ProgressModalDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking Delete Document eventually calls delete client api", () => {
|
||||||
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocuments.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
@@ -480,7 +496,8 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocuments).toHaveBeenCalled();
|
// The implementation uses setTimeout, so wait for it to finish
|
||||||
|
waitFor(() => expect(mockDeleteDocuments).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
import {
|
||||||
import { Dismiss16Filled } from "@fluentui/react-icons";
|
Button,
|
||||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
Link,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarBody,
|
||||||
|
MessageBarTitle,
|
||||||
|
TableRowId,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { 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";
|
||||||
@@ -16,6 +24,8 @@ import { Platform, configContext } from "ConfigContext";
|
|||||||
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 Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
@@ -35,7 +45,7 @@ import { QueryConstants } from "Shared/Constants";
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
@@ -60,6 +70,10 @@ import TabsBase from "../TabsBase";
|
|||||||
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
||||||
|
const NO_SQL_THROTTLING_DOC_URL =
|
||||||
|
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
||||||
|
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
||||||
|
const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer";
|
||||||
|
|
||||||
const loadMoreHeight = LayoutConstants.rowHeight;
|
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||||
export const useDocumentsTabStyles = makeStyles({
|
export const useDocumentsTabStyles = makeStyles({
|
||||||
@@ -76,12 +90,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,
|
||||||
},
|
},
|
||||||
@@ -91,6 +99,13 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
tableCell: {
|
tableCell: {
|
||||||
...cosmosShorthands.borderLeft(),
|
...cosmosShorthands.borderLeft(),
|
||||||
},
|
},
|
||||||
|
tableHeader: {
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
tableHeaderFiller: {
|
||||||
|
width: "20px",
|
||||||
|
boxShadow: `0px -1px ${tokens.colorNeutralStroke2} inset`,
|
||||||
|
},
|
||||||
loadMore: {
|
loadMore: {
|
||||||
...cosmosShorthands.borderTop(),
|
...cosmosShorthands.borderTop(),
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@@ -103,6 +118,20 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
...shorthands.outline("1px", "dotted"),
|
...shorthands.outline("1px", "dotted"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
floatingControlsContainer: {
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
floatingControls: {
|
||||||
|
position: "absolute",
|
||||||
|
top: "6px",
|
||||||
|
right: 0,
|
||||||
|
float: "right",
|
||||||
|
backgroundColor: "white",
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
deleteProgressContent: {
|
||||||
|
paddingTop: tokens.spacingVerticalL,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DocumentsTabV2 extends TabsBase {
|
export class DocumentsTabV2 extends TabsBase {
|
||||||
@@ -521,8 +550,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"`),
|
||||||
@@ -533,7 +560,7 @@ const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
|
|||||||
type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem };
|
type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem };
|
||||||
|
|
||||||
// This is based on some heuristics
|
// This is based on some heuristics
|
||||||
const calculateOffset = (columnNumber: number): number => columnNumber * 16 - 29;
|
const calculateOffset = (columnNumber: number): number => columnNumber * 16 - 27;
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
|
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
|
||||||
@@ -548,14 +575,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onIsExecutingChange,
|
onIsExecutingChange,
|
||||||
isTabActive,
|
isTabActive,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true);
|
|
||||||
const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false);
|
|
||||||
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
|
||||||
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
|
||||||
const [filterContent, setFilterContent] = useState<string>("");
|
const [filterContent, setFilterContent] = useState<string>("");
|
||||||
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
|
||||||
@@ -575,7 +597,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
// Table user clicked on this row
|
// Table user clicked on this row
|
||||||
const [clickedRowIndex, setClickedRowIndex] = useState<number>(RESET_INDEX);
|
const [clickedRowIndex, setClickedRowIndex] = useState<number>(RESET_INDEX);
|
||||||
// Table multiple selection
|
// Table multiple selection
|
||||||
const [selectedRows, setSelectedRows] = React.useState<Set<TableRowId>>(() => new Set<TableRowId>([0]));
|
const [selectedRows, setSelectedRows] = React.useState<Set<TableRowId>>(() => new Set<TableRowId>());
|
||||||
|
|
||||||
// Command buttons
|
// Command buttons
|
||||||
const [editorState, setEditorState] = useState<ViewModels.DocumentExplorerState>(
|
const [editorState, setEditorState] = useState<ViewModels.DocumentExplorerState>(
|
||||||
@@ -602,35 +624,118 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// For progress bar for bulk delete (noSql)
|
||||||
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = React.useState(false);
|
||||||
|
const [bulkDeleteProcess, setBulkDeleteProcess] = useState<{
|
||||||
|
pendingIds: DocumentId[];
|
||||||
|
successfulIds: DocumentId[];
|
||||||
|
throttledIds: DocumentId[];
|
||||||
|
failedIds: DocumentId[];
|
||||||
|
beforeExecuteMs: number; // Delay before executing delete. Used for retrying throttling after a specified delay
|
||||||
|
hasBeenThrottled: boolean; // Keep track if the operation has been throttled at least once
|
||||||
|
}>(undefined);
|
||||||
|
const [bulkDeleteOperation, setBulkDeleteOperation] = useState<{
|
||||||
|
onCompleted: (documentIds: DocumentId[]) => void;
|
||||||
|
onFailed: (reason?: unknown) => void;
|
||||||
|
count: number;
|
||||||
|
collection: CollectionBase;
|
||||||
|
}>(undefined);
|
||||||
|
const [bulkDeleteMode, setBulkDeleteMode] = useState<"inProgress" | "completed" | "aborting" | "aborted">(undefined);
|
||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* Recursion is implemented using React useEffect (as opposed to recursively calling setTimeout), because it
|
||||||
|
* has to update the <ProgressModalDialog> or check if the user is aborting the operation via state React
|
||||||
|
* variables.
|
||||||
|
*
|
||||||
|
* Inputs are the bulkDeleteOperation, bulkDeleteProcess and bulkDeleteMode state variables.
|
||||||
|
* When the bulkDeleteProcess changes, the function in the useEffect is triggered and checks if the process
|
||||||
|
* was aborted or completed, which will resolve the promise.
|
||||||
|
* Otherwise, it will attempt to delete documents of the pending and throttled ids arrays.
|
||||||
|
* Once deletion is completed, the function updates bulkDeleteProcess with the results, which will trigger
|
||||||
|
* the function to be called again.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFilterFocused) {
|
if (!bulkDeleteOperation || !bulkDeleteProcess || !bulkDeleteMode) {
|
||||||
filterInput.current?.focus();
|
return;
|
||||||
}
|
}
|
||||||
}, [isFilterFocused]);
|
|
||||||
|
|
||||||
// Clicked row must be defined
|
if (bulkDeleteMode === "completed" || bulkDeleteMode === "aborted") {
|
||||||
useEffect(() => {
|
// no op in the case function is called again
|
||||||
if (documentIds.length > 0) {
|
return;
|
||||||
let currentClickedRowIndex = clickedRowIndex;
|
|
||||||
if (
|
|
||||||
(currentClickedRowIndex === RESET_INDEX &&
|
|
||||||
editorState === ViewModels.DocumentExplorerState.noDocumentSelected) ||
|
|
||||||
currentClickedRowIndex > documentIds.length - 1
|
|
||||||
) {
|
|
||||||
// reset clicked row or the current clicked row is out of bounds
|
|
||||||
currentClickedRowIndex = INITIAL_SELECTED_ROW_INDEX;
|
|
||||||
setSelectedRows(new Set([INITIAL_SELECTED_ROW_INDEX]));
|
|
||||||
onDocumentClicked(currentClickedRowIndex, documentIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [documentIds, clickedRowIndex, editorState]);
|
|
||||||
|
|
||||||
const applyFilterButton = {
|
if (
|
||||||
enabled: true,
|
(bulkDeleteProcess.pendingIds.length === 0 && bulkDeleteProcess.throttledIds.length === 0) ||
|
||||||
visible: true,
|
bulkDeleteMode === "aborting"
|
||||||
};
|
) {
|
||||||
|
// Successfully deleted all documents or operation was aborted
|
||||||
|
bulkDeleteOperation.onCompleted(bulkDeleteProcess.successfulIds);
|
||||||
|
setBulkDeleteMode(bulkDeleteMode === "aborting" ? "aborted" : "completed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start deleting documents or retry throttled requests
|
||||||
|
const newPendingIds = bulkDeleteProcess.pendingIds.concat(bulkDeleteProcess.throttledIds);
|
||||||
|
const timeout = bulkDeleteProcess.beforeExecuteMs || 0;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
deleteNoSqlDocuments(bulkDeleteOperation.collection, [...newPendingIds])
|
||||||
|
.then((deleteResult) => {
|
||||||
|
let retryAfterMilliseconds = 0;
|
||||||
|
const newSuccessful: DocumentId[] = [];
|
||||||
|
const newThrottled: DocumentId[] = [];
|
||||||
|
const newFailed: DocumentId[] = [];
|
||||||
|
deleteResult.forEach((result) => {
|
||||||
|
if (result.statusCode === Constants.HttpStatusCodes.NoContent) {
|
||||||
|
newSuccessful.push(result.documentId);
|
||||||
|
} else if (result.statusCode === Constants.HttpStatusCodes.TooManyRequests) {
|
||||||
|
newThrottled.push(result.documentId);
|
||||||
|
retryAfterMilliseconds = Math.max(result.retryAfterMilliseconds, retryAfterMilliseconds);
|
||||||
|
} else if (result.statusCode >= 400) {
|
||||||
|
newFailed.push(result.documentId);
|
||||||
|
logConsoleError(
|
||||||
|
`Failed to delete document ${result.documentId.id} with status code ${result.statusCode}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logConsoleInfo(`Successfully deleted ${newSuccessful.length} document(s)`);
|
||||||
|
|
||||||
|
if (newThrottled.length > 0) {
|
||||||
|
logConsoleError(
|
||||||
|
`Failed to delete ${newThrottled.length} document(s) due to "Request too large" (429) error. Retrying...`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update result of the bulk delete: method is called again, because the state variables changed
|
||||||
|
// it will decide at the next call what to do
|
||||||
|
setBulkDeleteProcess((prev) => ({
|
||||||
|
pendingIds: [],
|
||||||
|
successfulIds: prev.successfulIds.concat(newSuccessful),
|
||||||
|
throttledIds: newThrottled,
|
||||||
|
failedIds: prev.failedIds.concat(newFailed),
|
||||||
|
beforeExecuteMs: retryAfterMilliseconds,
|
||||||
|
hasBeenThrottled: prev.hasBeenThrottled || newThrottled.length > 0,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error deleting documents", error);
|
||||||
|
setBulkDeleteProcess((prev) => ({
|
||||||
|
pendingIds: [],
|
||||||
|
throttledIds: [],
|
||||||
|
successfulIds: prev.successfulIds,
|
||||||
|
failedIds: prev.failedIds.concat(prev.pendingIds),
|
||||||
|
beforeExecuteMs: undefined,
|
||||||
|
hasBeenThrottled: prev.hasBeenThrottled,
|
||||||
|
}));
|
||||||
|
bulkDeleteOperation.onFailed(error);
|
||||||
|
});
|
||||||
|
}, timeout);
|
||||||
|
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
||||||
|
|
||||||
const partitionKey: DataModels.PartitionKey = useMemo(
|
const partitionKey: DataModels.PartitionKey = useMemo(
|
||||||
() => _partitionKey || (_collection && _collection.partitionKey),
|
() => _partitionKey || (_collection && _collection.partitionKey),
|
||||||
@@ -702,10 +807,6 @@ 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("");
|
setFilterContent("");
|
||||||
refreshDocumentsGrid(true);
|
refreshDocumentsGrid(true);
|
||||||
@@ -882,7 +983,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(() => setSelectedRows(new Set([documentIds.length - 1])))
|
.then(() => {
|
||||||
|
setSelectedRows(new Set([documentIds.length - 1]));
|
||||||
|
setClickedRowIndex(documentIds.length - 1);
|
||||||
|
})
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
}, [
|
}, [
|
||||||
onExecutionErrorChange,
|
onExecutionErrorChange,
|
||||||
@@ -976,8 +1080,36 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setSelectedDocumentContent(selectedDocumentContentBaseline);
|
setSelectedDocumentContent(selectedDocumentContentBaseline);
|
||||||
}, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]);
|
}, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a useEffect() to bulk delete noSql documents
|
||||||
|
* @param collection
|
||||||
|
* @param documentIds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const _bulkDeleteNoSqlDocuments = (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> =>
|
||||||
|
new Promise<DocumentId[]>((resolve, reject) => {
|
||||||
|
setBulkDeleteOperation({
|
||||||
|
onCompleted: resolve,
|
||||||
|
onFailed: reject,
|
||||||
|
count: documentIds.length,
|
||||||
|
collection,
|
||||||
|
});
|
||||||
|
setBulkDeleteProcess({
|
||||||
|
pendingIds: [...documentIds],
|
||||||
|
throttledIds: [],
|
||||||
|
successfulIds: [],
|
||||||
|
failedIds: [],
|
||||||
|
beforeExecuteMs: 0,
|
||||||
|
hasBeenThrottled: false,
|
||||||
|
});
|
||||||
|
setIsBulkDeleteDialogOpen(true);
|
||||||
|
setBulkDeleteMode("inProgress");
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation using bulk delete NoSQL API
|
* Implementation using bulk delete NoSQL API
|
||||||
|
* @param list of document ids to delete
|
||||||
|
* @returns Promise of list of deleted document ids
|
||||||
*/
|
*/
|
||||||
const _deleteDocuments = useCallback(
|
const _deleteDocuments = useCallback(
|
||||||
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
@@ -988,20 +1120,33 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
});
|
});
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
let deletePromise;
|
||||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
if (!isPreferredApiMongoDB) {
|
||||||
const _deleteNoSqlDocuments = async (
|
if (partitionKey.systemKey) {
|
||||||
collection: CollectionBase,
|
// ----------------------------------------------------------------------------------------------------
|
||||||
toDeleteDocumentIds: DocumentId[],
|
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
||||||
): Promise<DocumentId[]> => {
|
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should
|
||||||
return partitionKey.systemKey
|
// always be called for NoSQL.
|
||||||
? deleteNoSqlDocument(collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
deletePromise = deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => {
|
||||||
: deleteNoSqlDocuments(collection, toDeleteDocumentIds);
|
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
|
||||||
};
|
return [toDeleteDocumentIds[0]];
|
||||||
|
});
|
||||||
const deletePromise = !isPreferredApiMongoDB
|
// ----------------------------------------------------------------------------------------------------
|
||||||
? _deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
} else {
|
||||||
: MongoProxyClient.deleteDocuments(
|
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isMongoBulkDeleteDisabled) {
|
||||||
|
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
||||||
|
// MongoProxyClient.deleteDocuments() should be called for all users.
|
||||||
|
deletePromise = MongoProxyClient.deleteDocument(
|
||||||
|
_collection.databaseId,
|
||||||
|
_collection as ViewModels.Collection,
|
||||||
|
toDeleteDocumentIds[0],
|
||||||
|
).then(() => [toDeleteDocumentIds[0]]);
|
||||||
|
// ----------------------------------------------------------------------------------------------------
|
||||||
|
} else {
|
||||||
|
deletePromise = MongoProxyClient.deleteDocuments(
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
_collection as ViewModels.Collection,
|
_collection as ViewModels.Collection,
|
||||||
toDeleteDocumentIds,
|
toDeleteDocumentIds,
|
||||||
@@ -1011,6 +1156,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return deletePromise
|
return deletePromise
|
||||||
.then(
|
.then(
|
||||||
@@ -1041,9 +1188,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => {
|
||||||
|
setIsExecuting(false);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle],
|
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle, partitionKey.systemKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDocuments = useCallback(
|
const deleteDocuments = useCallback(
|
||||||
@@ -1061,14 +1210,25 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setClickedRowIndex(undefined);
|
setClickedRowIndex(undefined);
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkModalDialog("Delete documents", `${deletedIds.length} document(s) successfully deleted.`);
|
|
||||||
},
|
},
|
||||||
(error: Error) =>
|
(error: Error) => {
|
||||||
useDialog
|
if (error instanceof MongoProxyClient.ThrottlingError) {
|
||||||
.getState()
|
useDialog
|
||||||
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`),
|
.getState()
|
||||||
|
.showOkModalDialog(
|
||||||
|
"Delete documents",
|
||||||
|
`Some documents failed to delete due to a rate limiting error. Please try again later. To prevent this in the future, consider increasing the throughput on your container or database.`,
|
||||||
|
{
|
||||||
|
linkText: "Learn More",
|
||||||
|
linkUrl: MONGO_THROTTLING_DOC_URL,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
useDialog
|
||||||
|
.getState()
|
||||||
|
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`);
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
},
|
},
|
||||||
@@ -1129,12 +1289,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],
|
||||||
@@ -1176,19 +1330,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);
|
||||||
|
|
||||||
@@ -1330,14 +1471,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();
|
||||||
}
|
}
|
||||||
@@ -1835,10 +1971,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);
|
||||||
@@ -1853,6 +1985,26 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
[createIterator, filterContent],
|
[createIterator, filterContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While retrying, display: retrying now.
|
||||||
|
* If completed and all documents were deleted, display: all documents deleted.
|
||||||
|
* @returns 429 warning message
|
||||||
|
*/
|
||||||
|
const get429WarningMessageNoSql = (): string => {
|
||||||
|
let message = 'Some delete requests failed due to a "Request too large" exception (429)';
|
||||||
|
|
||||||
|
if (bulkDeleteOperation.count === bulkDeleteProcess.successfulIds.length) {
|
||||||
|
message += ", but were successfully retried.";
|
||||||
|
} else if (bulkDeleteMode === "inProgress" || bulkDeleteMode === "aborting") {
|
||||||
|
message += ". Retrying now.";
|
||||||
|
} else {
|
||||||
|
message += ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (message +=
|
||||||
|
" To prevent this in the future, consider increasing the throughput on your container or database.");
|
||||||
|
};
|
||||||
|
|
||||||
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||||
// Do not allow to unselecting all columns
|
// Do not allow to unselecting all columns
|
||||||
if (newSelectedColumnIds.length === 0) {
|
if (newSelectedColumnIds.length === 0) {
|
||||||
@@ -1888,169 +2040,183 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||||
|
|
||||||
|
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
||||||
|
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
||||||
|
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
||||||
|
const isBulkDeleteDisabled =
|
||||||
|
(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;
|
||||||
|
};
|
||||||
|
|
||||||
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 && (
|
<div className={styles.filterRow}>
|
||||||
<>
|
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||||
{!isFilterExpanded && !isPreferredApiMongoDB && (
|
<InputDataList
|
||||||
<div className={styles.filterRow}>
|
dropdownOptions={getFilterChoices()}
|
||||||
<span>SELECT * FROM c</span>
|
placeholder={
|
||||||
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
isPreferredApiMongoDB
|
||||||
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||||
Edit Filter
|
: "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."
|
||||||
</Button>
|
}
|
||||||
</div>
|
title="Type a query predicate or choose one from the list."
|
||||||
)}
|
value={filterContent}
|
||||||
{!isFilterExpanded && isPreferredApiMongoDB && (
|
onChange={(value) => setFilterContent(value)}
|
||||||
<div className={styles.filterRow}>
|
onKeyDown={onFilterKeyDown}
|
||||||
{appliedFilter.length > 0 && <span>Filter :</span>}
|
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||||
{!(appliedFilter.length > 0) && <span className="noFilterApplied">No filter applied</span>}
|
/>
|
||||||
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
<Button
|
||||||
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
appearance="primary"
|
||||||
Edit Filter
|
size="small"
|
||||||
</Button>
|
onClick={() => {
|
||||||
</div>
|
if (isExecuting) {
|
||||||
)}
|
if (!isPreferredApiMongoDB) {
|
||||||
{isFilterExpanded && (
|
queryAbortController.abort();
|
||||||
<div className={styles.filterRow}>
|
}
|
||||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
} else {
|
||||||
<Input
|
onApplyFilterClick();
|
||||||
ref={filterInput}
|
}
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
list={`filtersList-${getUniqueId(_collection)}`}
|
|
||||||
className={`filterInput ${styles.filterInput}`}
|
|
||||||
title="Type a query predicate or choose one from the list."
|
|
||||||
placeholder={
|
|
||||||
isPreferredApiMongoDB
|
|
||||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
|
||||||
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
|
||||||
}
|
|
||||||
value={filterContent}
|
|
||||||
autoFocus={true}
|
|
||||||
onKeyDown={onFilterKeyDown}
|
|
||||||
onChange={(e) => setFilterContent(e.target.value)}
|
|
||||||
onBlur={() => setIsFilterFocused(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
|
||||||
{addStringsNoDuplicate(
|
|
||||||
lastFilterContents,
|
|
||||||
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
|
||||||
).map((filter) => (
|
|
||||||
<option key={filter} value={filter} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
appearance="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={onApplyFilterClick}
|
|
||||||
disabled={!applyFilterButton.enabled}
|
|
||||||
aria-label="Apply filter"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
{!isPreferredApiMongoDB && isExecuting && (
|
|
||||||
<Button
|
|
||||||
appearance="primary"
|
|
||||||
size="small"
|
|
||||||
aria-label="Cancel Query"
|
|
||||||
onClick={() => queryAbortController.abort()}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Cancel Query
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
aria-label="close filter"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={onHideFilterClick}
|
|
||||||
onKeyDown={onCloseButtonKeyDown}
|
|
||||||
appearance="transparent"
|
|
||||||
size="small"
|
|
||||||
icon={<Dismiss16Filled />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* <Split> doesn't like to be a flex child */}
|
|
||||||
<div style={{ overflow: "hidden", height: "100%" }}>
|
|
||||||
<Allotment
|
|
||||||
onDragEnd={(sizes: number[]) => {
|
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
|
||||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
|
||||||
setTabStateData(tabStateData);
|
|
||||||
}}
|
}}
|
||||||
|
disabled={isExecuting && isPreferredApiMongoDB}
|
||||||
|
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
|
||||||
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
</Button>
|
||||||
<div className={styles.tableContainer}>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
height: "100%",
|
|
||||||
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
|
||||||
} /* Fix to make table not resize beyond parent's width */
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DocumentsTableComponent
|
|
||||||
onRefreshTable={() => refreshDocumentsGrid(false)}
|
|
||||||
items={tableItems}
|
|
||||||
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
size={tableContainerSizePx}
|
|
||||||
selectedColumnIds={selectedColumnIds}
|
|
||||||
columnDefinitions={columnDefinitions}
|
|
||||||
isRowSelectionDisabled={
|
|
||||||
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
|
||||||
}
|
|
||||||
onColumnSelectionChange={onColumnSelectionChange}
|
|
||||||
defaultColumnSelection={getInitialColumnSelection()}
|
|
||||||
collection={_collection}
|
|
||||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{tableItems.length > 0 && (
|
|
||||||
<a
|
|
||||||
className={styles.loadMore}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
|
||||||
onKeyDown={onLoadMoreKeyInput}
|
|
||||||
>
|
|
||||||
Load more
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Allotment.Pane>
|
|
||||||
<Allotment.Pane minSize={30}>
|
|
||||||
<div style={{ height: "100%", width: "100%" }}>
|
|
||||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
|
||||||
<EditorReact
|
|
||||||
language={"json"}
|
|
||||||
content={selectedDocumentContent}
|
|
||||||
isReadOnly={false}
|
|
||||||
ariaLabel={"Document editor"}
|
|
||||||
lineNumbers={"on"}
|
|
||||||
theme={"_theme"}
|
|
||||||
onContentChanged={_onEditorContentChange}
|
|
||||||
enableWordWrapContextMenuItem={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedRows.size > 1 && (
|
|
||||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Allotment.Pane>
|
|
||||||
</Allotment>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Allotment
|
||||||
|
onDragEnd={(sizes: number[]) => {
|
||||||
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
|
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
|
setTabStateData(tabStateData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||||
|
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
height: "100%",
|
||||||
|
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
||||||
|
} /* Fix to make table not resize beyond parent's width */
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DocumentsTableComponent
|
||||||
|
onRefreshTable={() => refreshDocumentsGrid(false)}
|
||||||
|
items={tableItems}
|
||||||
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
size={tableContainerSizePx}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
columnDefinitions={columnDefinitions}
|
||||||
|
isRowSelectionDisabled={
|
||||||
|
isBulkDeleteDisabled ||
|
||||||
|
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||||
|
}
|
||||||
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
|
collection={_collection}
|
||||||
|
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tableItems.length > 0 && (
|
||||||
|
<a
|
||||||
|
className={styles.loadMore}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
||||||
|
onKeyDown={onLoadMoreKeyInput}
|
||||||
|
>
|
||||||
|
Load more
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Allotment.Pane>
|
||||||
|
<Allotment.Pane minSize={30}>
|
||||||
|
<div style={{ height: "100%", width: "100%" }}>
|
||||||
|
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={selectedDocumentContent}
|
||||||
|
isReadOnly={false}
|
||||||
|
ariaLabel={"Document editor"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
theme={"_theme"}
|
||||||
|
onContentChanged={_onEditorContentChange}
|
||||||
|
enableWordWrapContextMenuItem={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedRows.size > 1 && (
|
||||||
|
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Allotment.Pane>
|
||||||
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
|
{bulkDeleteOperation && (
|
||||||
|
<ProgressModalDialog
|
||||||
|
isOpen={isBulkDeleteDialogOpen}
|
||||||
|
dismissText="Abort"
|
||||||
|
onDismiss={() => {
|
||||||
|
setIsBulkDeleteDialogOpen(false);
|
||||||
|
setBulkDeleteOperation(undefined);
|
||||||
|
}}
|
||||||
|
onCancel={() => setBulkDeleteMode("aborting")}
|
||||||
|
title={`Deleting ${bulkDeleteOperation.count} document(s)`}
|
||||||
|
message={`Successfully deleted ${bulkDeleteProcess.successfulIds.length} document(s).`}
|
||||||
|
maxValue={bulkDeleteOperation.count}
|
||||||
|
value={bulkDeleteProcess.successfulIds.length}
|
||||||
|
mode={bulkDeleteMode}
|
||||||
|
>
|
||||||
|
<div className={styles.deleteProgressContent}>
|
||||||
|
{(bulkDeleteMode === "aborting" || bulkDeleteMode === "aborted") && (
|
||||||
|
<div style={{ paddingBottom: tokens.spacingVerticalL }}>Deleting document(s) was aborted.</div>
|
||||||
|
)}
|
||||||
|
{(bulkDeleteProcess.failedIds.length > 0 ||
|
||||||
|
(bulkDeleteProcess.throttledIds.length > 0 && bulkDeleteMode !== "inProgress")) && (
|
||||||
|
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalL }}>
|
||||||
|
<MessageBarBody>
|
||||||
|
<MessageBarTitle>Error</MessageBarTitle>
|
||||||
|
Failed to delete{" "}
|
||||||
|
{bulkDeleteMode === "inProgress"
|
||||||
|
? bulkDeleteProcess.failedIds.length
|
||||||
|
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length}{" "}
|
||||||
|
document(s).
|
||||||
|
</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
{bulkDeleteProcess.hasBeenThrottled && (
|
||||||
|
<MessageBar intent="warning">
|
||||||
|
<MessageBarBody>
|
||||||
|
<MessageBarTitle>Warning</MessageBarTitle>
|
||||||
|
{get429WarningMessageNoSql()}{" "}
|
||||||
|
<Link href={NO_SQL_THROTTLING_DOC_URL} target="_blank">
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ProgressModalDialog>
|
||||||
|
)}
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ jest.mock("Common/MongoProxyClient", () => ({
|
|||||||
id: "id1",
|
id: "id1",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
deleteDocuments: jest.fn(() => Promise.resolve()),
|
deleteDocuments: jest.fn(() => Promise.resolve({ deleteCount: 0, isAcknowledged: true })),
|
||||||
|
ThrottlingError: Error,
|
||||||
|
useMongoProxyEndpoint: jest.fn(() => true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
@@ -65,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 () => {
|
||||||
@@ -178,7 +187,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document asks for confirmation", () => {
|
it("clicking Delete Document eventually calls delete client api", () => {
|
||||||
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocuments.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ describe("DocumentsTableComponent", () => {
|
|||||||
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
|
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
|
||||||
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
|
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
|
||||||
],
|
],
|
||||||
onItemClicked: (): void => {},
|
|
||||||
onSelectedRowsChange: (): void => {},
|
onSelectedRowsChange: (): void => {},
|
||||||
selectedRows: new Set<TableRowId>(),
|
selectedRows: new Set<TableRowId>(),
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ import {
|
|||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
@@ -69,7 +68,6 @@ export type ColumnDefinition = {
|
|||||||
export interface IDocumentsTableComponentProps {
|
export interface IDocumentsTableComponentProps {
|
||||||
onRefreshTable: () => void;
|
onRefreshTable: () => void;
|
||||||
items: DocumentsTableComponentItem[];
|
items: DocumentsTableComponentItem[];
|
||||||
onItemClicked: (index: number) => void;
|
|
||||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
size: { height: number; width: number };
|
size: { height: number; width: number };
|
||||||
@@ -99,6 +97,7 @@ const defaultSize = {
|
|||||||
idealWidth: 200,
|
idealWidth: 200,
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
onRefreshTable,
|
onRefreshTable,
|
||||||
items,
|
items,
|
||||||
@@ -116,6 +115,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
|
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 = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
@@ -228,55 +229,53 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||||
Refresh
|
Refresh
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{userContext.features.enableDocumentsTableColumnSelection && (
|
<>
|
||||||
<>
|
<MenuItem
|
||||||
<MenuItem
|
icon={<TextSortAscendingRegular />}
|
||||||
icon={<TextSortAscendingRegular />}
|
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||||
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
>
|
||||||
>
|
Sort ascending
|
||||||
Sort ascending
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<TextSortDescendingRegular />}
|
||||||
|
onClick={(e) => onSortClick(e, column.id, "descending")}
|
||||||
|
>
|
||||||
|
Sort descending
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
||||||
|
Reset sorting
|
||||||
|
</MenuItem>
|
||||||
|
{!isColumnSelectionDisabled && (
|
||||||
|
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
||||||
|
Edit columns
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
)}
|
||||||
icon={<TextSortDescendingRegular />}
|
<MenuDivider />
|
||||||
onClick={(e) => onSortClick(e, column.id, "descending")}
|
</>
|
||||||
>
|
<MenuItem
|
||||||
Sort descending
|
key="keyboardresize"
|
||||||
</MenuItem>
|
icon={<TableResizeColumnRegular />}
|
||||||
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
onClick={columnSizing.enableKeyboardMode(column.id)}
|
||||||
Reset sorting
|
>
|
||||||
</MenuItem>
|
Resize with left/right arrow keys
|
||||||
{!isColumnSelectionDisabled && (
|
</MenuItem>
|
||||||
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
{!isColumnSelectionDisabled && (
|
||||||
Edit columns
|
<MenuItem
|
||||||
</MenuItem>
|
key="remove"
|
||||||
)}
|
icon={<DeleteRegular />}
|
||||||
<MenuDivider />
|
onClick={() => {
|
||||||
<MenuItem
|
// Remove column id from selectedColumnIds
|
||||||
key="keyboardresize"
|
const index = selectedColumnIds.indexOf(column.id);
|
||||||
icon={<TableResizeColumnRegular />}
|
if (index === -1) {
|
||||||
onClick={columnSizing.enableKeyboardMode(column.id)}
|
return;
|
||||||
>
|
}
|
||||||
Resize with left/right arrow keys
|
const newSelectedColumnIds = [...selectedColumnIds];
|
||||||
</MenuItem>
|
newSelectedColumnIds.splice(index, 1);
|
||||||
{!isColumnSelectionDisabled && (
|
onColumnSelectionChange(newSelectedColumnIds);
|
||||||
<MenuItem
|
}}
|
||||||
key="remove"
|
>
|
||||||
icon={<DeleteRegular />}
|
Remove column
|
||||||
onClick={() => {
|
</MenuItem>
|
||||||
// Remove column id from selectedColumnIds
|
|
||||||
const index = selectedColumnIds.indexOf(column.id);
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newSelectedColumnIds = [...selectedColumnIds];
|
|
||||||
newSelectedColumnIds.splice(index, 1);
|
|
||||||
onColumnSelectionChange(newSelectedColumnIds);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove column
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</MenuPopover>
|
</MenuPopover>
|
||||||
@@ -294,22 +293,42 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
|
|
||||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
||||||
const onTableCellClicked = useCallback(
|
const onTableCellClicked = useCallback(
|
||||||
(e: React.MouseEvent, index: number) => {
|
(e: React.MouseEvent | undefined, index: number, rowId: TableRowId) => {
|
||||||
if (isSelectionDisabled) {
|
if (isSelectionDisabled) {
|
||||||
// Only allow click
|
// Only allow click
|
||||||
onSelectedRowsChange(new Set<TableRowId>([index]));
|
onSelectedRowsChange(new Set<TableRowId>([rowId]));
|
||||||
setSelectionStartIndex(index);
|
setSelectionStartIndex(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The selection helper computes in the index space (what's visible to the user in the table, ie the sorted array).
|
||||||
|
// selectedRows is in the rowId space (the index of the original unsorted array), so it must be converted to the index space.
|
||||||
|
const selectedRowsIndex = new Set<number>();
|
||||||
|
selectedRows.forEach((rowId) => {
|
||||||
|
const index = sortedRowsRef.current.findIndex((row: TableRowData) => row.rowId === rowId);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectedRowsIndex.add(index);
|
||||||
|
} else {
|
||||||
|
// This should never happen
|
||||||
|
console.error(`Row with rowId ${rowId} not found in sorted rows`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const result = selectionHelper(
|
const result = selectionHelper(
|
||||||
selectedRows as Set<number>,
|
selectedRowsIndex,
|
||||||
index,
|
index,
|
||||||
isEnvironmentShiftPressed(e),
|
e && isEnvironmentShiftPressed(e),
|
||||||
isEnvironmentCtrlPressed(e),
|
e && isEnvironmentCtrlPressed(e),
|
||||||
selectionStartIndex,
|
selectionStartIndex,
|
||||||
);
|
);
|
||||||
onSelectedRowsChange(result.selection);
|
|
||||||
|
// Convert selectionHelper result from index space back to rowId space
|
||||||
|
const selectedRowIds = new Set<TableRowId>();
|
||||||
|
result.selection.forEach((index) => {
|
||||||
|
selectedRowIds.add(sortedRowsRef.current[index].rowId);
|
||||||
|
});
|
||||||
|
onSelectedRowsChange(selectedRowIds);
|
||||||
|
|
||||||
if (result.selectionStartIndex !== undefined) {
|
if (result.selectionStartIndex !== undefined) {
|
||||||
setSelectionStartIndex(result.selectionStartIndex);
|
setSelectionStartIndex(result.selectionStartIndex);
|
||||||
}
|
}
|
||||||
@@ -323,16 +342,20 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
* - a key is down and the cell is clicked by the mouse
|
* - a key is down and the cell is clicked by the mouse
|
||||||
*/
|
*/
|
||||||
const onIdClicked = useCallback(
|
const onIdClicked = useCallback(
|
||||||
(e: React.KeyboardEvent<Element>, index: number) => {
|
(e: React.KeyboardEvent<Element>, rowId: TableRowId) => {
|
||||||
if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) {
|
if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) {
|
||||||
onSelectedRowsChange(new Set<TableRowId>([index]));
|
onSelectedRowsChange(new Set<TableRowId>([rowId]));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onSelectedRowsChange],
|
[onSelectedRowsChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => {
|
const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => {
|
||||||
const { item, selected, appearance, onClick, onKeyDown } = data[index];
|
// WARNING: because the table sorts the data, 'index' is not the same as 'rowId'
|
||||||
|
// The rowId is the index of the item in the original array,
|
||||||
|
// while the index is the index of the item in the sorted array
|
||||||
|
const { item, selected, appearance, onClick, onKeyDown, rowId } = data[index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
aria-rowindex={index + 2}
|
aria-rowindex={index + 2}
|
||||||
@@ -362,8 +385,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
key={column.columnId}
|
key={column.columnId}
|
||||||
className={styles.tableCell}
|
className={styles.tableCell}
|
||||||
// When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick.
|
// When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick.
|
||||||
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index)}
|
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index, rowId)}
|
||||||
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, index)}
|
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, rowId)}
|
||||||
{...columnSizing.getTableCellProps(column.columnId)}
|
{...columnSizing.getTableCellProps(column.columnId)}
|
||||||
tabIndex={column.columnId === "id" ? 0 : -1}
|
tabIndex={column.columnId === "id" ? 0 : -1}
|
||||||
>
|
>
|
||||||
@@ -423,6 +446,19 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Store the sorted rows in a ref which won't trigger a re-render (as opposed to a state)
|
||||||
|
sortedRowsRef.current = rows;
|
||||||
|
|
||||||
|
// If there are no selected rows, auto select the first row
|
||||||
|
const [autoSelectFirstDoc, setAutoSelectFirstDoc] = React.useState<boolean>(true);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (autoSelectFirstDoc && sortedRowsRef.current?.length > 0 && selectedRows.size === 0) {
|
||||||
|
setAutoSelectFirstDoc(false);
|
||||||
|
const DOC_INDEX_TO_SELECT = 0;
|
||||||
|
onTableCellClicked(undefined, DOC_INDEX_TO_SELECT, sortedRowsRef.current[DOC_INDEX_TO_SELECT].rowId);
|
||||||
|
}
|
||||||
|
}, [selectedRows, onTableCellClicked, autoSelectFirstDoc]);
|
||||||
|
|
||||||
const toggleAllKeydown = React.useCallback(
|
const toggleAllKeydown = React.useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
@@ -471,8 +507,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table noNativeElements sortable {...tableProps}>
|
<Table noNativeElements {...tableProps}>
|
||||||
<TableHeader>
|
<TableHeader className={styles.tableHeader}>
|
||||||
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
||||||
{!isSelectionDisabled && (
|
{!isSelectionDisabled && (
|
||||||
<TableSelectionCell
|
<TableSelectionCell
|
||||||
@@ -494,6 +530,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<div className={styles.tableHeaderFiller}></div>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -17,111 +17,124 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
||||||
>
|
>
|
||||||
<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
|
<Allotment
|
||||||
style={
|
onDragEnd={[Function]}
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"overflow": "hidden",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Allotment
|
<Allotment.Pane
|
||||||
onDragEnd={[Function]}
|
minSize={55}
|
||||||
|
preferredSize="35%"
|
||||||
>
|
>
|
||||||
<Allotment.Pane
|
<div
|
||||||
minSize={55}
|
style={
|
||||||
preferredSize="35%"
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
className="___9o87uj0_0000000 ffefeo0"
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"overflow": "hidden",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
style={
|
||||||
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"width": "calc(100% + -11px)",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<DocumentsTableComponent
|
||||||
style={
|
collection={
|
||||||
{
|
{
|
||||||
"height": "100%",
|
"databaseId": "databaseId",
|
||||||
"width": "calc(100% + -13px)",
|
"id": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
columnDefinitions={
|
||||||
<DocumentsTableComponent
|
[
|
||||||
collection={
|
|
||||||
{
|
{
|
||||||
"databaseId": "databaseId",
|
"id": "id",
|
||||||
"id": [Function],
|
"isPartitionKey": false,
|
||||||
}
|
"label": "id",
|
||||||
}
|
},
|
||||||
columnDefinitions={
|
]
|
||||||
[
|
}
|
||||||
{
|
defaultColumnSelection={
|
||||||
"id": "id",
|
[
|
||||||
"isPartitionKey": false,
|
"id",
|
||||||
"label": "id",
|
]
|
||||||
},
|
}
|
||||||
]
|
isColumnSelectionDisabled={false}
|
||||||
}
|
isRowSelectionDisabled={true}
|
||||||
defaultColumnSelection={
|
items={[]}
|
||||||
[
|
onColumnSelectionChange={[Function]}
|
||||||
"id",
|
onRefreshTable={[Function]}
|
||||||
]
|
onSelectedRowsChange={[Function]}
|
||||||
}
|
selectedColumnIds={
|
||||||
isColumnSelectionDisabled={false}
|
[
|
||||||
isRowSelectionDisabled={true}
|
"id",
|
||||||
items={[]}
|
]
|
||||||
onColumnSelectionChange={[Function]}
|
}
|
||||||
onItemClicked={[Function]}
|
selectedRows={Set {}}
|
||||||
onRefreshTable={[Function]}
|
/>
|
||||||
onSelectedRowsChange={[Function]}
|
|
||||||
selectedColumnIds={
|
|
||||||
[
|
|
||||||
"id",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
selectedRows={
|
|
||||||
Set {
|
|
||||||
0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Allotment.Pane>
|
</div>
|
||||||
<Allotment.Pane
|
</Allotment.Pane>
|
||||||
minSize={30}
|
<Allotment.Pane
|
||||||
>
|
minSize={30}
|
||||||
<div
|
>
|
||||||
style={
|
<div
|
||||||
{
|
style={
|
||||||
"height": "100%",
|
{
|
||||||
"width": "100%",
|
"height": "100%",
|
||||||
}
|
"width": "100%",
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
</Allotment.Pane>
|
/>
|
||||||
</Allotment>
|
</Allotment.Pane>
|
||||||
</div>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
onItemClicked={[Function]}
|
|
||||||
onRefreshTable={[Function]}
|
onRefreshTable={[Function]}
|
||||||
onSelectedRowsChange={[Function]}
|
onSelectedRowsChange={[Function]}
|
||||||
selectedColumnIds={[]}
|
selectedColumnIds={[]}
|
||||||
@@ -57,7 +56,6 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
noNativeElements={true}
|
noNativeElements={true}
|
||||||
role="grid"
|
role="grid"
|
||||||
size="small"
|
size="small"
|
||||||
sortable={true}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"minWidth": "fit-content",
|
"minWidth": "fit-content",
|
||||||
@@ -75,9 +73,11 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader
|
||||||
|
className="___1gzszts_0000000 f22iagw"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TableHeader ___oeyxrt0_1baslyg ftgm304"
|
className="fui-TableHeader ___1gzszts_39qb7g0 f22iagw"
|
||||||
role="rowgroup"
|
role="rowgroup"
|
||||||
>
|
>
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -98,6 +98,9 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<div
|
||||||
|
className="___1ndi7nn_0000000 f64fuq3 f1ppkcfa"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -500,7 +503,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
onItemClicked={[Function]}
|
|
||||||
onRefreshTable={[Function]}
|
onRefreshTable={[Function]}
|
||||||
onSelectedRowsChange={[Function]}
|
onSelectedRowsChange={[Function]}
|
||||||
selectedColumnIds={[]}
|
selectedColumnIds={[]}
|
||||||
@@ -518,7 +520,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
noNativeElements={true}
|
noNativeElements={true}
|
||||||
role="grid"
|
role="grid"
|
||||||
size="small"
|
size="small"
|
||||||
sortable={true}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"minWidth": "fit-content",
|
"minWidth": "fit-content",
|
||||||
@@ -536,9 +537,11 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader
|
||||||
|
className="___1gzszts_0000000 f22iagw"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TableHeader ___oeyxrt0_1baslyg ftgm304"
|
className="fui-TableHeader ___1gzszts_39qb7g0 f22iagw"
|
||||||
role="rowgroup"
|
role="rowgroup"
|
||||||
>
|
>
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -601,6 +604,9 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
</TableSelectionCell>
|
</TableSelectionCell>
|
||||||
</div>
|
</div>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<div
|
||||||
|
className="___1ndi7nn_0000000 f64fuq3 f1ppkcfa"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
constructor(props: IMongoShellTabComponentProps) {
|
constructor(props: IMongoShellTabComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._logTraces = new Map();
|
this._logTraces = new Map();
|
||||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
|
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
createTableColumn,
|
createTableColumn,
|
||||||
tokens,
|
tokens,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { ErrorCircleFilled, MoreHorizontalRegular, WarningFilled } from "@fluentui/react-icons";
|
import { ErrorCircleFilled, MoreHorizontalRegular, QuestionRegular, WarningFilled } from "@fluentui/react-icons";
|
||||||
import QueryError, { QueryErrorSeverity, compareSeverity } from "Common/QueryError";
|
import QueryError, { QueryErrorSeverity, compareSeverity } from "Common/QueryError";
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||||
@@ -34,25 +34,32 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "code",
|
columnId: "code",
|
||||||
compare: (item1, item2) => item1.code.localeCompare(item2.code),
|
compare: (item1, item2) => item1.code.localeCompare(item2.code),
|
||||||
renderHeaderCell: () => null,
|
renderHeaderCell: () => "Code",
|
||||||
renderCell: (item) => item.code,
|
renderCell: (item) => <TableCellLayout truncate>{item.code}</TableCellLayout>,
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "severity",
|
columnId: "severity",
|
||||||
compare: (item1, item2) => compareSeverity(item1.severity, item2.severity),
|
compare: (item1, item2) => compareSeverity(item1.severity, item2.severity),
|
||||||
renderHeaderCell: () => null,
|
renderHeaderCell: () => "Severity",
|
||||||
renderCell: (item) => <TableCellLayout media={severityIcons[item.severity]}>{item.severity}</TableCellLayout>,
|
renderCell: (item) => (
|
||||||
|
<TableCellLayout truncate media={severityIcons[item.severity]}>
|
||||||
|
{item.severity}
|
||||||
|
</TableCellLayout>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "location",
|
columnId: "location",
|
||||||
compare: (item1, item2) => item1.location?.start?.offset - item2.location?.start?.offset,
|
compare: (item1, item2) => item1.location?.start?.offset - item2.location?.start?.offset,
|
||||||
renderHeaderCell: () => "Location",
|
renderHeaderCell: () => "Location",
|
||||||
renderCell: (item) =>
|
renderCell: (item) => (
|
||||||
item.location
|
<TableCellLayout truncate>
|
||||||
? item.location.start.lineNumber
|
{item.location
|
||||||
? `Line ${item.location.start.lineNumber}`
|
? item.location.start.lineNumber
|
||||||
: "<unknown>"
|
? `Line ${item.location.start.lineNumber}`
|
||||||
: "<no location>",
|
: "<unknown>"
|
||||||
|
: "<no location>"}
|
||||||
|
</TableCellLayout>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "message",
|
columnId: "message",
|
||||||
@@ -60,8 +67,20 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
renderHeaderCell: () => "Message",
|
renderHeaderCell: () => "Message",
|
||||||
renderCell: (item) => (
|
renderCell: (item) => (
|
||||||
<div className={styles.errorListMessageCell}>
|
<div className={styles.errorListMessageCell}>
|
||||||
<div className={styles.errorListMessage}>{item.message}</div>
|
<div className={styles.errorListMessage} title={item.message}>
|
||||||
<div>
|
{item.message}
|
||||||
|
</div>
|
||||||
|
<div className={styles.errorListMessageActions}>
|
||||||
|
{item.helpLink && (
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
aria-label="Help"
|
||||||
|
appearance="subtle"
|
||||||
|
icon={<QuestionRegular />}
|
||||||
|
href={item.helpLink}
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
aria-label="Details"
|
aria-label="Details"
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
@@ -76,9 +95,9 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
|
|
||||||
const columnSizingOptions: TableColumnSizingOptions = {
|
const columnSizingOptions: TableColumnSizingOptions = {
|
||||||
code: {
|
code: {
|
||||||
minWidth: 75,
|
minWidth: 90,
|
||||||
idealWidth: 75,
|
idealWidth: 90,
|
||||||
defaultWidth: 75,
|
defaultWidth: 90,
|
||||||
},
|
},
|
||||||
severity: {
|
severity: {
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { fireEvent, render } from "@testing-library/react";
|
|||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
IQueryTabComponentProps,
|
IQueryTabComponentProps,
|
||||||
QueryTabComponent,
|
QueryTabComponent,
|
||||||
QueryTabCopilotComponent,
|
QueryTabCopilotComponent,
|
||||||
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
|
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -16,6 +18,24 @@ import React from "react";
|
|||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact");
|
jest.mock("Explorer/Controls/Editor/EditorReact");
|
||||||
|
|
||||||
|
const loadState = (path: StorePath) => {
|
||||||
|
if (
|
||||||
|
path.componentName === AppStateComponentNames.QueryCopilot &&
|
||||||
|
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||||
|
loadState,
|
||||||
|
AppStateComponentNames: {
|
||||||
|
QueryCopilot: "QueryCopilot",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
const mockStore = useQueryCopilot.getState();
|
const mockStore = useQueryCopilot.getState();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -32,7 +52,7 @@ describe("QueryTabComponent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||||
collection: { databaseId: "CopilotSampleDb" },
|
collection: { databaseId: "CopilotSampleDB" },
|
||||||
onTabAccessor: () => jest.fn(),
|
onTabAccessor: () => jest.fn(),
|
||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
tabId: "mockTabId",
|
tabId: "mockTabId",
|
||||||
@@ -50,6 +70,17 @@ describe("QueryTabComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copilot should be enabled by default when tab is active", () => {
|
it("copilot should be enabled by default when tab is active", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "name",
|
||||||
|
properties: undefined,
|
||||||
|
id: "",
|
||||||
|
location: "",
|
||||||
|
type: "",
|
||||||
|
kind: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useQueryCopilot.getState().setCopilotEnabled(true);
|
useQueryCopilot.getState().setCopilotEnabled(true);
|
||||||
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
||||||
const activeTab = new TabsBase({
|
const activeTab = new TabsBase({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { monaco } from "Explorer/LazyMonaco";
|
|||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
@@ -46,7 +47,6 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
|||||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import * as StringUtility from "../../../Shared/StringUtility";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
@@ -209,13 +209,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
|
||||||
);
|
|
||||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
|
||||||
: true;
|
|
||||||
return copilotInitialActive;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -584,7 +578,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _toggleCopilot = (active: boolean) => {
|
private _toggleCopilot = (active: boolean) => {
|
||||||
this.setState({ copilotActive: active });
|
this.setState({ copilotActive: active });
|
||||||
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
||||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
|
saveCopilotToggleStatus(userContext.databaseAccount, active);
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
||||||
databaseName: this.props.collection.databaseId,
|
databaseName: this.props.collection.databaseId,
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
metricsGridButtons: {
|
metricsGridButtons: {
|
||||||
...cosmosShorthands.borderTop(),
|
...cosmosShorthands.borderTop(),
|
||||||
},
|
},
|
||||||
|
errorListTableCell: {
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
errorListMessageCell: {
|
errorListMessageCell: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -80,5 +85,12 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
errorListMessage: {
|
errorListMessage: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
errorListMessageActions: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IMessageBarStyles, Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { IpRule } from "Contracts/DataModels";
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
@@ -16,7 +16,6 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
|||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
@@ -37,9 +36,6 @@ 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, networkSettingsWarning } = useTabs();
|
||||||
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
|
|
||||||
userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(),
|
|
||||||
);
|
|
||||||
const [
|
const [
|
||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
@@ -87,29 +83,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
{networkSettingsWarning}
|
{networkSettingsWarning}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
{showRUThresholdMessageBar && (
|
|
||||||
<MessageBar
|
|
||||||
messageBarType={MessageBarType.info}
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowRUThresholdMessageBar(false);
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
...defaultMessageBarStyles,
|
|
||||||
innerText: {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`Data Explorer has a 5,000 RU default limit. To adjust the limit, go to the Settings page and find "RU Threshold".`}
|
|
||||||
<Link
|
|
||||||
className="underlinedLink"
|
|
||||||
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user