mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 11:51:07 +00:00
Compare commits
55 Commits
ashleyst/r
...
before_opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd905403b | ||
|
|
bd592d07af | ||
|
|
644f5941ec | ||
|
|
9fb006a996 | ||
|
|
c2b98c3e23 | ||
|
|
76d49d86d4 | ||
|
|
7893b89bf7 | ||
|
|
5945e3cb6b | ||
|
|
213d1c68fe | ||
|
|
c26f9a1ebb | ||
|
|
bd7cd7ae8f | ||
|
|
6504358580 | ||
|
|
ce88659fca | ||
|
|
642c708e9c | ||
|
|
4156009d09 | ||
|
|
5c6abbd635 | ||
|
|
881726e9af | ||
|
|
7015590d1a | ||
|
|
1d952a4ea2 | ||
|
|
2a81551a60 | ||
|
|
eceee36913 | ||
|
|
96faf92c12 | ||
|
|
2fdb3df4ae | ||
|
|
7c9802c07d | ||
|
|
e5609bd91e | ||
|
|
4b75e86b74 | ||
|
|
abf061089d | ||
|
|
ec25586a6e | ||
|
|
c15d1432b2 | ||
|
|
73d2686025 | ||
|
|
80b926214b | ||
|
|
070b7a4ca7 | ||
|
|
d61ff5dcb5 | ||
|
|
d42eebaa5a | ||
|
|
056be2a74d | ||
|
|
b93c90e7d1 | ||
|
|
82de81f2b6 | ||
|
|
236f075cf6 | ||
|
|
d478af3869 | ||
|
|
93c1fdc238 | ||
|
|
d562fc0f40 | ||
|
|
808faa9fa5 | ||
|
|
c1bc11d27d | ||
|
|
ac2e2a6f8e | ||
|
|
3138580eae | ||
|
|
aa88815c6e | ||
|
|
5a2f78b51e | ||
|
|
fbc2e1335b | ||
|
|
eb0d7b71b3 | ||
|
|
261289b031 | ||
|
|
fae4589427 | ||
|
|
cbcb7e6240 | ||
|
|
e0b773d920 | ||
|
|
9ec2cea95c | ||
|
|
1a4f713a79 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
||||
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
|
||||
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build:contracts
|
||||
- name: Restore Build Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
@@ -92,18 +92,20 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- run: cp -r ./Contracts ./dist/contracts
|
||||
- run: cp -r ./configs ./dist/configs
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: "Az CLI login"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
|
||||
- name: Upload build to preview blob storage
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
|
||||
- name: Upload preview config to blob storage
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -113,21 +115,21 @@ jobs:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: packages
|
||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||
- run: dotnet nuget remove source "ADO"
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload package to Artifacts
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
name: prod-package
|
||||
path: "bin/Release/*.nupkg"
|
||||
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -137,22 +139,21 @@ jobs:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/mpac.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: packages
|
||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||
- run: dotnet nuget remove source "ADO"
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload package to Artifacts
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
name: mpac-package
|
||||
path: "bin/Release/*.nupkg"
|
||||
|
||||
playwright-tests:
|
||||
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
|
||||
@@ -185,9 +186,9 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
|
||||
merge-playwright-reports:
|
||||
name: "Merge Playwright Reports"
|
||||
@@ -197,26 +198,26 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
9
DataExplorer.proj
Normal file
9
DataExplorer.proj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<NoBuild>true</NoBuild>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<NuspecFile>DataExplorer.nuspec</NuspecFile>
|
||||
<NuspecProperties>version=$(PackageVersion)</NuspecProperties>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
||||
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
@@ -18,7 +19,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
|
||||
### Hosted Development (https://cosmos.azure.com)
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</a>
|
||||
<ul>
|
||||
<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>
|
||||
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
||||
<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
|
||||
transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"],
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!@fluentui/react-icons|(.*)/dist/browser)/",
|
||||
"/node_modules/plotly.js-cartesian-dist-min",
|
||||
"/externals/",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
@@ -1830,6 +1830,14 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.customAccordion button:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.customAccordion {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.datalist-arrow:after:hover {
|
||||
content: "\276F";
|
||||
position: absolute;
|
||||
@@ -3117,3 +3125,7 @@ a:link {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebarContainer {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.splashLoaderContainer {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#divExplorer {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@@ -27,26 +31,24 @@ a:focus {
|
||||
.resourceTreeAndTabs {
|
||||
border-radius: 0px;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.tabsManagerContainer {
|
||||
background-color: #ffffff
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
padding-top: 5px;
|
||||
background-color: #ffffff
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.commandBarContainer {
|
||||
background-color: #ffffff;
|
||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
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;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
||||
border-bottom: 2px solid @FabricAccentMedium;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||
border-bottom: 0px none transparent;
|
||||
}
|
||||
|
||||
@@ -94,10 +95,10 @@ a:focus {
|
||||
padding-bottom: @SmallSpace;
|
||||
|
||||
.contentWrapper {
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIconSection {
|
||||
.cancelButton {
|
||||
@@ -119,7 +120,6 @@ a:focus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.resourceTree {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -156,25 +156,21 @@ a:focus {
|
||||
}
|
||||
|
||||
.selected {
|
||||
&>.treeNodeHeader {
|
||||
& > .treeNodeHeader {
|
||||
background-color: @FabricAccentExtra;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dataExplorerErrorConsoleContainer {
|
||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 0px;
|
||||
width: auto;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.filterbtnstyle {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
@@ -200,12 +196,10 @@ a:focus {
|
||||
border: solid 1px #d1d1d1;
|
||||
}
|
||||
|
||||
|
||||
.gridRowSelected .tabdocumentsGridElement:hover {
|
||||
background-color: @FabricAccentLight !important;
|
||||
}
|
||||
|
||||
|
||||
.refreshcol {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
@@ -216,4 +210,4 @@ a:focus {
|
||||
|
||||
.fileImportImg img {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
}
|
||||
|
||||
765
package-lock.json
generated
765
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -5,10 +5,9 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.0.1-beta.3",
|
||||
"@azure/cosmos": "4.2.0-beta.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.5.2",
|
||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
@@ -117,7 +116,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.44.0",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -170,10 +169,10 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "3.0.2",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "11.1.3",
|
||||
"less-vars-loader": "1.1.0",
|
||||
@@ -247,4 +246,4 @@
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[defaults]
|
||||
group = stfaul
|
||||
sku = P1v2
|
||||
appserviceplan = stfaul_asp_Linux_centralus_0
|
||||
location = centralus
|
||||
web = cosmos-explorer-preview
|
||||
group = dataexplorer-preview
|
||||
sku = P1V2
|
||||
appserviceplan = dataexplorer-preview
|
||||
location = westus2
|
||||
web = dataexplorer-preview
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
|
||||
|
||||
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
|
||||
|
||||
Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||
|
||||
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"PROXY_PATH": "/proxy",
|
||||
"msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
|
||||
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
const port = process.env.PORT || 3000;
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const api = createProxyMiddleware("/api", {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
||||
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
|
||||
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
||||
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
|
||||
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
||||
|
||||
const api = createProxyMiddleware({
|
||||
target: backendEndpoint,
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: (req, res) => {
|
||||
@@ -15,8 +22,8 @@ const api = createProxyMiddleware("/api", {
|
||||
},
|
||||
});
|
||||
|
||||
const proxy = createProxyMiddleware("/proxy", {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
const proxy = createProxyMiddleware({
|
||||
target: backendEndpoint,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
@@ -27,35 +34,38 @@ const proxy = createProxyMiddleware("/proxy", {
|
||||
},
|
||||
});
|
||||
|
||||
const commit = createProxyMiddleware("/commit", {
|
||||
target: "https://cosmosexplorerpreview.blob.core.windows.net",
|
||||
const commit = createProxyMiddleware({
|
||||
target: previewStorageWebsiteEndpoint,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
pathRewrite: { "^/commit": "$web/" },
|
||||
pathRewrite: { "^/commit": "/" },
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(api);
|
||||
app.use(proxy);
|
||||
app.use(commit);
|
||||
app.use("/api", api);
|
||||
app.use("/proxy", proxy);
|
||||
app.use("/commit", commit);
|
||||
app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
const pr = req.params.pr;
|
||||
if (!/^\d+$/.test(pr)) {
|
||||
return res.status(400).send("Invalid pull request number");
|
||||
}
|
||||
const [, query] = req.originalUrl.split("?");
|
||||
const search = new URLSearchParams(query);
|
||||
|
||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
|
||||
fetch(`${githubApiUrl}/pulls/${pr}`)
|
||||
.then((response) => response.json())
|
||||
.then(({ head: { ref, sha } }) => {
|
||||
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
|
||||
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
|
||||
prUrl.hash = ref;
|
||||
search.set("feature.pr", prUrl.href);
|
||||
|
||||
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
|
||||
explorer.search = search.toString();
|
||||
|
||||
const portal = new URL("https://ms.portal.azure.com/");
|
||||
const portal = new URL(azurePortalMpacEndpoint);
|
||||
portal.searchParams.set("dataExplorerSource", explorer.href);
|
||||
|
||||
return res.redirect(portal.href);
|
||||
@@ -63,12 +73,10 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
.catch(() => res.sendStatus(500));
|
||||
});
|
||||
app.get("/", (req, res) => {
|
||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
|
||||
fetch(`${githubApiUrl}/branches/master`)
|
||||
.then((response) => response.json())
|
||||
.then(({ commit: { sha } }) => {
|
||||
const explorer = new URL(
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html"
|
||||
);
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`);
|
||||
return res.redirect(explorer.href);
|
||||
})
|
||||
.catch(() => res.sendStatus(500));
|
||||
|
||||
1360
preview/package-lock.json
generated
1360
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -12,7 +12,8 @@
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^1.1.0",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"node": "^18.20.6",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ export class CapabilityNames {
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
||||
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
|
||||
}
|
||||
|
||||
export enum CapacityMode {
|
||||
@@ -96,6 +97,12 @@ export enum CapacityMode {
|
||||
Serverless = "Serverless",
|
||||
}
|
||||
|
||||
export enum WorkloadType {
|
||||
Learning = "Learning",
|
||||
DevelopmentTesting = "Development/Testing",
|
||||
Production = "Production",
|
||||
None = "None",
|
||||
}
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
@@ -118,6 +125,7 @@ export class AfecFeatures {
|
||||
|
||||
export class TagNames {
|
||||
public static defaultExperience: string = "defaultExperience";
|
||||
public static WorkloadType: string = "hidden-workload-type";
|
||||
}
|
||||
|
||||
export class MongoDBAccounts {
|
||||
@@ -148,13 +156,25 @@ export class PortalBackendEndpoints {
|
||||
}
|
||||
|
||||
export class MongoProxyEndpoints {
|
||||
public static readonly Local: string = "https://localhost:7238";
|
||||
public static readonly Development: string = "https://localhost:7238";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||
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 {
|
||||
public static readonly Development: string = "https://localhost:7240";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||
@@ -505,6 +525,11 @@ export class PriorityLevel {
|
||||
public static readonly Default = "low";
|
||||
}
|
||||
|
||||
export class ariaLabelForLearnMoreLink {
|
||||
public static readonly AnalyticalStore = "Learn more about analytical store.";
|
||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
|
||||
@@ -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 { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
||||
|
||||
@@ -20,22 +21,22 @@ describe("getTokenFromAuthService", () => {
|
||||
|
||||
it("builds the correct URL in production", () => {
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
});
|
||||
getTokenFromAuthService("GET", "dbs", "foo");
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct URL in dev", () => {
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
||||
});
|
||||
getTokenFromAuthService("GET", "dbs", "foo");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
||||
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -78,7 +79,7 @@ describe("requestPlugin", () => {
|
||||
const next = jest.fn();
|
||||
updateConfigContext({
|
||||
platform: Platform.Hosted,
|
||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
PROXY_PATH: "/proxy",
|
||||
});
|
||||
const headers = {};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AuthType } from "../AuthType";
|
||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
@@ -27,7 +27,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
);
|
||||
if (!userContext.aadToken) {
|
||||
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;
|
||||
}
|
||||
@@ -189,10 +189,19 @@ let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) {
|
||||
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
||||
if (!userContext.refreshCosmosClient) {
|
||||
return _client;
|
||||
}
|
||||
_client.dispose();
|
||||
_client = null;
|
||||
}
|
||||
|
||||
if (userContext.refreshCosmosClient) {
|
||||
updateUserContext({
|
||||
refreshCosmosClient: false,
|
||||
});
|
||||
}
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { WorkloadType } from "Common/Constants";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { DatabaseAccount, Tags } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
|
||||
describe("Database Account Utility", () => {
|
||||
describe("Workload Type", () => {
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
it("Workload Type should return Learning", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {
|
||||
"hidden-workload-type": WorkloadType.Learning,
|
||||
} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.Learning);
|
||||
});
|
||||
|
||||
it("Workload Type should return None", () => {
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.None);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TagNames, WorkloadType } from "Common/Constants";
|
||||
import { Tags } from "Contracts/DataModels";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
function isVirtualNetworkFilterEnabled() {
|
||||
@@ -15,3 +17,12 @@ function isPrivateEndpointConnectionsEnabled() {
|
||||
export function isPublicInternetAccessAllowed(): boolean {
|
||||
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
||||
}
|
||||
|
||||
export function getWorkloadType(): WorkloadType {
|
||||
const tags: Tags = userContext?.databaseAccount?.tags;
|
||||
const workloadType: WorkloadType = tags && (tags[TagNames.WorkloadType] as WorkloadType);
|
||||
if (!workloadType) {
|
||||
return WorkloadType.None;
|
||||
}
|
||||
return workloadType;
|
||||
}
|
||||
|
||||
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;
|
||||
entityTimeValue: string;
|
||||
entityValueType: string;
|
||||
entityProperty: string;
|
||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
onSelectDate: (date: Date | null | undefined) => void;
|
||||
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
@@ -26,6 +27,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
onSelectDate,
|
||||
isEntityValueDisable,
|
||||
onEntityTimeValueChange,
|
||||
entityProperty,
|
||||
}: TableEntityProps): JSX.Element => {
|
||||
if (isEntityTypeDate) {
|
||||
return (
|
||||
@@ -51,15 +53,20 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label={entityValueLabel && entityValueLabel}
|
||||
className="addEntityTextField"
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={typeof entityValue === "string" ? entityValue : ""}
|
||||
onChange={onEntityValueChange}
|
||||
ariaLabel={attributeValueLabel}
|
||||
/>
|
||||
<>
|
||||
<span id={entityProperty} className="screenReaderOnly">
|
||||
Edit Property {entityProperty} {attributeValueLabel}
|
||||
</span>
|
||||
<TextField
|
||||
label={entityValueLabel && entityValueLabel}
|
||||
className="addEntityTextField"
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={typeof entityValue === "string" ? entityValue : ""}
|
||||
onChange={onEntityValueChange}
|
||||
aria-labelledby={entityProperty}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MongoProxyEndpoints } from "Common/Constants";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
@@ -71,7 +72,8 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -82,16 +84,19 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
queryDocuments(databaseId, collection, true, "{}");
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
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, "{}");
|
||||
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),
|
||||
);
|
||||
});
|
||||
@@ -103,7 +108,8 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -114,16 +120,19 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
readDocument(databaseId, collection, documentId);
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
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),
|
||||
);
|
||||
});
|
||||
@@ -135,7 +144,8 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -146,16 +156,19 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
readDocument(databaseId, collection, documentId);
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
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),
|
||||
);
|
||||
});
|
||||
@@ -167,7 +180,8 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -178,16 +192,19 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
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, "{}");
|
||||
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),
|
||||
);
|
||||
});
|
||||
@@ -199,7 +216,8 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -210,16 +228,19 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
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),
|
||||
);
|
||||
});
|
||||
@@ -231,13 +252,14 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
@@ -249,18 +271,20 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
@@ -272,12 +296,12 @@ describe("MongoProxyClient", () => {
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
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", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import {
|
||||
allowedMongoProxyEndpoints,
|
||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
import queryString from "querystring";
|
||||
@@ -14,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export function queryDocuments(
|
||||
query,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -194,7 +194,7 @@ export function readDocument(
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint("readDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -217,7 +217,7 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -289,7 +289,7 @@ export function createDocument(
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint("createDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -308,7 +308,7 @@ export function createDocument(
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createDocument`, {
|
||||
@@ -373,7 +373,7 @@ export function updateDocument(
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint("updateDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -396,7 +396,7 @@ export function updateDocument(
|
||||
: "",
|
||||
documentContent,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -464,7 +464,7 @@ export function updateDocument_ToBeDeprecated(
|
||||
}
|
||||
|
||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||
if (!useMongoProxyEndpoint("deleteDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) {
|
||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -486,7 +486,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -561,7 +561,10 @@ export function deleteDocuments(
|
||||
const { databaseAccount } = userContext;
|
||||
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 = {
|
||||
databaseID: databaseId,
|
||||
@@ -572,7 +575,7 @@ export function deleteDocuments(
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
@@ -596,7 +599,7 @@ export function deleteDocuments(
|
||||
export function createMongoCollectionWithProxy(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -619,7 +622,7 @@ export function createMongoCollectionWithProxy(
|
||||
isSharded: !!shardKey,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createCollection`, {
|
||||
@@ -689,12 +692,13 @@ export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
if (useMongoProxyEndpoint(feature)) {
|
||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
} else {
|
||||
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||
...defaultAllowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
];
|
||||
endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
||||
...allowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
])
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
@@ -715,19 +719,82 @@ export function getEndpoint(endpoint: string): string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
];
|
||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||
[MongoProxyApi.ResourceList]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.QueryDocuments]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.ReadDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.UpdateDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.DeleteDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.LegacyMongoShell]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.BulkDelete]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
);
|
||||
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||
}
|
||||
|
||||
export class ThrottlingError extends Error {
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("QueryError.tryParse", () => {
|
||||
code: "BadRequest",
|
||||
message: "Your query is bad, and you should feel bad",
|
||||
};
|
||||
const message = JSON.stringify(innerError);
|
||||
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||
const outerError = {
|
||||
code: "BadRequest",
|
||||
message,
|
||||
@@ -48,7 +48,7 @@ describe("QueryError.tryParse", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message.
|
||||
// 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 = [
|
||||
{
|
||||
@@ -69,7 +69,7 @@ describe("QueryError.tryParse", () => {
|
||||
message: "Your query is bad, and you should feel bad",
|
||||
errors,
|
||||
};
|
||||
const message = JSON.stringify(innerError);
|
||||
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||
const outerError = {
|
||||
code: "BadRequest",
|
||||
message,
|
||||
@@ -91,4 +91,23 @@ describe("QueryError.tryParse", () => {
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
// 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 }),
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,16 +214,28 @@ export default class QueryError {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
||||
if (message.startsWith("Message: ")) {
|
||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
||||
// So we use a separate variable to avoid this.
|
||||
message = message.substring("Message: ".length);
|
||||
// Some newer backends produce a message that contains a doubly-nested JSON payload.
|
||||
// In this case, the message we get is a fully-complete JSON object we can parse.
|
||||
// So let's try that first
|
||||
if (message.startsWith("{") && message.endsWith("}")) {
|
||||
let outer: unknown = undefined;
|
||||
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");
|
||||
message = lines[0].trim();
|
||||
|
||||
if (message.startsWith("Message: ")) {
|
||||
message = message.substring("Message: ".length);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(message);
|
||||
|
||||
@@ -135,6 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
onEntityValueChange={onEntityValueChange}
|
||||
onSelectDate={onSelectDate}
|
||||
onEntityTimeValueChange={onEntityTimeValueChange}
|
||||
entityProperty={entityProperty}
|
||||
/>
|
||||
{!isEntityValueDisable && (
|
||||
<TooltipHost content="Edit property" id="editTooltip">
|
||||
|
||||
@@ -99,6 +99,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
if (params.vectorEmbeddingPolicy) {
|
||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||
}
|
||||
if (params.fullTextPolicy) {
|
||||
resource.fullTextPolicy = params.fullTextPolicy;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
@@ -270,6 +273,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||
analyticalStorageTtl: params.analyticalStorageTtl,
|
||||
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
||||
fullTextPolicy: params.fullTextPolicy,
|
||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||
const collectionOptions: RequestOptions = {};
|
||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||
|
||||
@@ -8,16 +8,16 @@ import {
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedCassandraProxyEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMongoProxyEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
defaultAllowedCassandraProxyEndpoints,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedArmEndpoints: ReadonlyArray<string>;
|
||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
@@ -53,7 +55,6 @@ export interface ConfigContext {
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
NEW_MONGO_APIS?: string[];
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
PROXY_PATH?: string;
|
||||
@@ -66,6 +67,8 @@ export interface ConfigContext {
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
msalRedirectURI?: string;
|
||||
globallyEnabledCassandraAPIs?: string[];
|
||||
globallyEnabledMongoAPIs?: string[];
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
@@ -73,9 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.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]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
@@ -85,7 +91,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
@@ -106,21 +112,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
NEW_MONGO_APIS: [
|
||||
"resourcelist",
|
||||
"queryDocuments",
|
||||
"createDocument",
|
||||
"readDocument",
|
||||
"updateDocument",
|
||||
"deleteDocument",
|
||||
"createCollectionWithProxy",
|
||||
"legacyMongoShell",
|
||||
// "bulkdelete",
|
||||
],
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
globallyEnabledCassandraAPIs: [],
|
||||
globallyEnabledMongoAPIs: [],
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@@ -164,7 +161,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -172,7 +174,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum TabKind {
|
||||
Graph,
|
||||
SQLQuery,
|
||||
ScaleSettings,
|
||||
MongoQuery,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +52,8 @@ export interface OpenCollectionTab extends OpenTab {
|
||||
*/
|
||||
export interface OpenQueryTab extends OpenCollectionTab {
|
||||
query: QueryInfo;
|
||||
splitterDirection?: "vertical" | "horizontal";
|
||||
queryViewSizePercent?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ArmEntity {
|
||||
location: string;
|
||||
type: string;
|
||||
kind: string;
|
||||
tags?: Tags;
|
||||
}
|
||||
|
||||
export interface DatabaseAccount extends ArmEntity {
|
||||
@@ -159,6 +160,7 @@ export interface Collection extends Resource {
|
||||
analyticalStorageTtl?: number;
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
computedProperties?: ComputedProperties;
|
||||
@@ -199,11 +201,19 @@ export interface IndexingPolicy {
|
||||
compositeIndexes?: any[];
|
||||
spatialIndexes?: any[];
|
||||
vectorIndexes?: VectorIndex[];
|
||||
fullTextIndexes?: FullTextIndex[];
|
||||
}
|
||||
|
||||
export interface VectorIndex {
|
||||
path: string;
|
||||
type: "flat" | "diskANN" | "quantizedFlat";
|
||||
diskANNShardKey?: string;
|
||||
indexingSearchListSize?: number;
|
||||
quantizationByteSize?: number;
|
||||
}
|
||||
|
||||
export interface FullTextIndex {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ComputedProperty {
|
||||
@@ -342,6 +352,7 @@ export interface CreateCollectionParams {
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
createMongoWildcardIndex?: boolean;
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicy {
|
||||
@@ -355,6 +366,16 @@ export interface VectorEmbedding {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface FullTextPolicy {
|
||||
defaultLanguage: string;
|
||||
fullTextPaths: FullTextPath[];
|
||||
}
|
||||
|
||||
export interface FullTextPath {
|
||||
path: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface ReadDatabaseOfferParams {
|
||||
databaseId: string;
|
||||
databaseResourceId?: string;
|
||||
@@ -643,3 +664,5 @@ export interface FeatureRegistration {
|
||||
state: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Tags = { [key: string]: string };
|
||||
|
||||
@@ -41,7 +41,7 @@ export enum MessageTypes {
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
DisplayNPSSurvey,
|
||||
DisplayNPSSurvey, // unused
|
||||
OpenVCoreMongoNetworkingBlade,
|
||||
OpenVCoreMongoConnectionStringsBlade,
|
||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
||||
|
||||
@@ -115,7 +115,13 @@ export interface CollectionBase extends TreeNode {
|
||||
isSampleCollection?: boolean;
|
||||
|
||||
onDocumentDBDocumentsClick(): void;
|
||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
onNewQueryClick(
|
||||
source: any,
|
||||
event?: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
): void;
|
||||
expandCollection(): void;
|
||||
collapseCollection(): void;
|
||||
getDatabase(): Database;
|
||||
@@ -126,6 +132,8 @@ export interface Collection extends CollectionBase {
|
||||
analyticalStorageTtl: ko.Observable<number>;
|
||||
schema?: DataModels.ISchema;
|
||||
requestSchema?: () => void;
|
||||
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
||||
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
usageSizeInKB: ko.Observable<number>;
|
||||
@@ -149,7 +157,13 @@ export interface Collection extends CollectionBase {
|
||||
onSettingsClick: () => Promise<void>;
|
||||
|
||||
onNewGraphClick(): void;
|
||||
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
onNewMongoQueryClick(
|
||||
source: any,
|
||||
event?: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
): void;
|
||||
onNewMongoShellClick(): void;
|
||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||
@@ -309,6 +323,8 @@ export interface QueryTabOptions extends TabOptions {
|
||||
partitionKey?: DataModels.PartitionKey;
|
||||
queryText?: string;
|
||||
resourceTokenPartitionKey?: string;
|
||||
splitterDirection?: "horizontal" | "vertical";
|
||||
queryViewSizePercent?: number;
|
||||
}
|
||||
|
||||
export interface ScriptTabOption extends TabOptions {
|
||||
@@ -382,6 +398,8 @@ export interface DataExplorerInputsFrame {
|
||||
databaseAccount: any;
|
||||
subscriptionId?: string;
|
||||
resourceGroup?: string;
|
||||
tenantId?: string;
|
||||
userName?: string;
|
||||
masterKey?: string;
|
||||
hasWriteAccess?: boolean;
|
||||
authorizationToken?: string;
|
||||
|
||||
@@ -56,13 +56,15 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
),
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
@@ -146,14 +148,15 @@ export const createCollectionContextMenuButton = (
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () => {
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
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 { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
@@ -9,6 +9,9 @@ export interface CollapsibleSectionProps {
|
||||
onExpand?: () => void;
|
||||
children: JSX.Element;
|
||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||
showDelete?: boolean;
|
||||
onDelete?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CollapsibleSectionState {
|
||||
@@ -69,6 +72,20 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
)}
|
||||
{this.props.showDelete && (
|
||||
<Stack.Item style={{ marginLeft: "auto" }}>
|
||||
<IconButton
|
||||
disabled={this.props.disabled}
|
||||
id={`delete-${this.props.title.split(" ").join("-")}`}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27, marginRight: "20px" }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
this.props.onDelete();
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
{this.state.isExpanded && this.props.children}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
describe("AddFullTextPolicyForm", () => {
|
||||
//CTODO: add tests
|
||||
it.skip("should render correctly", () => {});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IStyleFunctionOrObject,
|
||||
ITextFieldStyleProps,
|
||||
ITextFieldStyles,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import * as React from "react";
|
||||
|
||||
export interface FullTextPoliciesComponentProps {
|
||||
fullTextPolicy: FullTextPolicy;
|
||||
onFullTextPathChange: (
|
||||
fullTextPolicy: FullTextPolicy,
|
||||
fullTextIndexes: FullTextIndex[],
|
||||
validationPassed: boolean,
|
||||
) => void;
|
||||
discardChanges?: boolean;
|
||||
onChangesDiscarded?: () => void;
|
||||
}
|
||||
|
||||
export interface FullTextPolicyData {
|
||||
path: string;
|
||||
language: string;
|
||||
pathError: string;
|
||||
}
|
||||
|
||||
const labelStyles = {
|
||||
root: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||
fieldGroup: {
|
||||
height: 27,
|
||||
},
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownStyles = {
|
||||
title: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
fontSize: 12,
|
||||
},
|
||||
dropdown: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
},
|
||||
dropdownItem: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPoliciesComponentProps> = ({
|
||||
fullTextPolicy,
|
||||
onFullTextPathChange,
|
||||
discardChanges,
|
||||
onChangesDiscarded,
|
||||
}): JSX.Element => {
|
||||
const getFullTextPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
if (!path) {
|
||||
error = "Full text path should not be empty";
|
||||
}
|
||||
if (
|
||||
index >= 0 &&
|
||||
fullTextPathData?.find(
|
||||
(fullTextPath: FullTextPolicyData, dataIndex: number) => dataIndex !== index && fullTextPath.path === path,
|
||||
)
|
||||
) {
|
||||
error = "Full text path is already defined";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const initializeData = (fullTextPolicy: FullTextPolicy): FullTextPolicyData[] => {
|
||||
if (!fullTextPolicy) {
|
||||
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
|
||||
}
|
||||
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
|
||||
...fullTextPath,
|
||||
pathError: getFullTextPathError(fullTextPath.path),
|
||||
}));
|
||||
};
|
||||
|
||||
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
|
||||
const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
|
||||
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
propagateData();
|
||||
}, [fullTextPathData, defaultLanguage]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (discardChanges) {
|
||||
setFullTextPathData(initializeData(fullTextPolicy));
|
||||
setDefaultLanguage(fullTextPolicy.defaultLanguage);
|
||||
onChangesDiscarded();
|
||||
}
|
||||
}, [discardChanges]);
|
||||
|
||||
const propagateData = () => {
|
||||
const newFullTextPolicy: FullTextPolicy = {
|
||||
defaultLanguage: defaultLanguage,
|
||||
fullTextPaths: fullTextPathData.map((policy: FullTextPolicyData) => ({
|
||||
path: policy.path,
|
||||
language: policy.language,
|
||||
})),
|
||||
};
|
||||
const fullTextIndexes: FullTextIndex[] = fullTextPathData.map((policy) => ({
|
||||
path: policy.path,
|
||||
}));
|
||||
const validationPassed = fullTextPathData.every((policy: FullTextPolicyData) => policy.pathError === "");
|
||||
onFullTextPathChange(newFullTextPolicy, fullTextIndexes, validationPassed);
|
||||
};
|
||||
|
||||
const onFullTextPathValueChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
const fullTextPaths = [...fullTextPathData];
|
||||
if (!fullTextPaths[index]?.path && !value.startsWith("/")) {
|
||||
fullTextPaths[index].path = "/" + value;
|
||||
} else {
|
||||
fullTextPaths[index].path = value;
|
||||
}
|
||||
fullTextPaths[index].pathError = getFullTextPathError(value, index);
|
||||
setFullTextPathData(fullTextPaths);
|
||||
};
|
||||
|
||||
const onFullTextPathPolicyChange = (index: number, option: IDropdownOption): void => {
|
||||
const policies = [...fullTextPathData];
|
||||
policies[index].language = option.key as never;
|
||||
setFullTextPathData(policies);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
setFullTextPathData([
|
||||
...fullTextPathData,
|
||||
{
|
||||
path: "",
|
||||
language: defaultLanguage,
|
||||
pathError: getFullTextPathError(""),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
const policies = fullTextPathData.filter((_uniqueKey, j) => index !== j);
|
||||
setFullTextPathData(policies);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack style={{ marginBottom: 10 }}>
|
||||
<Label styles={labelStyles}>Default language</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions()}
|
||||
selectedKey={defaultLanguage}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
setDefaultLanguage(option.key as never)
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
{fullTextPathData &&
|
||||
fullTextPathData.length > 0 &&
|
||||
fullTextPathData.map((fullTextPolicy: FullTextPolicyData, index: number) => (
|
||||
<CollapsibleSectionComponent
|
||||
key={index}
|
||||
isExpandedByDefault={true}
|
||||
title={`Full text path ${index + 1}`}
|
||||
showDelete={true}
|
||||
onDelete={() => onDelete(index)}
|
||||
>
|
||||
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||
<Stack
|
||||
styles={{
|
||||
root: {
|
||||
margin: "0 0 6px 20px !important",
|
||||
paddingLeft: 20,
|
||||
width: "80%",
|
||||
borderLeft: "1px solid",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Label styles={labelStyles}>Path</Label>
|
||||
<TextField
|
||||
id={`full-text-policy-path-${index + 1}`}
|
||||
required={true}
|
||||
placeholder="/fullTextPath1"
|
||||
styles={textFieldStyles}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onFullTextPathValueChange(index, event)}
|
||||
value={fullTextPolicy.path || ""}
|
||||
errorMessage={fullTextPolicy.pathError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={labelStyles}>Language</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions()}
|
||||
selectedKey={fullTextPolicy.language}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onFullTextPathPolicyChange(index, option)
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
))}
|
||||
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||
Add full text path
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
|
||||
return [
|
||||
{
|
||||
key: "en-US",
|
||||
text: "English (US)",
|
||||
},
|
||||
];
|
||||
};
|
||||
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
// This component is used to create a dropdown list of options for the user to select from.
|
||||
// The options are displayed in a dropdown list when the user clicks on the input field.
|
||||
// The user can then select an option from the list. The selected option is then displayed in the input field.
|
||||
|
||||
import { getTheme } from "@fluentui/react";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Input,
|
||||
Link,
|
||||
makeStyles,
|
||||
Popover,
|
||||
PopoverProps,
|
||||
PopoverSurface,
|
||||
PositioningImperativeRef,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons";
|
||||
import { NormalizedEventKey } from "Common/Constants";
|
||||
import { tokens } from "Explorer/Theme/ThemeUtil";
|
||||
import React, { FC, useEffect, useRef } from "react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
padding: 0,
|
||||
},
|
||||
input: {
|
||||
flexGrow: 1,
|
||||
paddingRight: 0,
|
||||
outline: "none",
|
||||
"& input:focus": {
|
||||
outline: "none", // Undo body :focus dashed outline
|
||||
},
|
||||
},
|
||||
inputButton: {
|
||||
border: 0,
|
||||
},
|
||||
dropdownHeader: {
|
||||
width: "100%",
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontWeight: 600,
|
||||
padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`,
|
||||
},
|
||||
dropdownStack: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
marginTop: tokens.spacingVerticalS,
|
||||
marginBottom: "1px",
|
||||
},
|
||||
dropdownOption: {
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontWeight: 400,
|
||||
justifyContent: "left",
|
||||
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
border: 0,
|
||||
":hover": {
|
||||
outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`,
|
||||
backgroundColor: tokens.colorNeutralBackground2Hover,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
},
|
||||
},
|
||||
bottomSection: {
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontWeight: 400,
|
||||
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
});
|
||||
|
||||
export interface InputDatalistDropdownOptionSection {
|
||||
label: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface InputDataListProps {
|
||||
dropdownOptions: InputDatalistDropdownOptionSection[];
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
autofocus?: boolean; // true: acquire focus on first render
|
||||
bottomLink?: {
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const InputDataList: FC<InputDataListProps> = ({
|
||||
dropdownOptions,
|
||||
placeholder,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
autofocus,
|
||||
bottomLink,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const [showDropdown, setShowDropdown] = React.useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const positioningRef = React.useRef<PositioningImperativeRef>(null);
|
||||
const [isInputFocused, setIsInputFocused] = React.useState(autofocus);
|
||||
const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false);
|
||||
|
||||
const theme = getTheme();
|
||||
const itemRefs = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
positioningRef.current?.setTarget(inputRef.current);
|
||||
}
|
||||
}, [inputRef, positioningRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInputFocused) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [isInputFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autofocusFirstDropdownItem && showDropdown) {
|
||||
// Autofocus on first item if input isn't focused
|
||||
itemRefs.current[0]?.focus();
|
||||
setAutofocusFirstDropdownItem(false);
|
||||
}
|
||||
}, [autofocusFirstDropdownItem, showDropdown]);
|
||||
|
||||
const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => {
|
||||
if (isInputFocused && !data.open) {
|
||||
// Don't close if input is focused and we're opening the dropdown (which will steal the focus)
|
||||
return;
|
||||
}
|
||||
|
||||
setShowDropdown(data.open || false);
|
||||
if (data.open) {
|
||||
setIsInputFocused(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === NormalizedEventKey.Escape) {
|
||||
setShowDropdown(false);
|
||||
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||
setShowDropdown(true);
|
||||
setAutofocusFirstDropdownItem(true);
|
||||
}
|
||||
onKeyDown(e);
|
||||
};
|
||||
|
||||
const handleDownDropdownItemKeyDown = (
|
||||
e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||
index: number,
|
||||
) => {
|
||||
if (e.key === NormalizedEventKey.Enter) {
|
||||
e.currentTarget.click();
|
||||
} else if (e.key === NormalizedEventKey.Escape) {
|
||||
setShowDropdown(false);
|
||||
inputRef.current?.focus();
|
||||
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||
if (index + 1 < itemRefs.current.length) {
|
||||
itemRefs.current[index + 1].focus();
|
||||
} else {
|
||||
setIsInputFocused(true);
|
||||
}
|
||||
} else if (e.key === NormalizedEventKey.UpArrow) {
|
||||
if (index - 1 >= 0) {
|
||||
itemRefs.current[index - 1].focus();
|
||||
} else {
|
||||
// Last item, focus back to input
|
||||
setIsInputFocused(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Flatten dropdownOptions to better manage refs and focus
|
||||
let flatIndex = 0;
|
||||
const indexMap = new Map<string, number>();
|
||||
for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) {
|
||||
const section = dropdownOptions[sectionIndex];
|
||||
for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) {
|
||||
indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex);
|
||||
flatIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="filterInput"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
className={`filterInput ${styles.input}`}
|
||||
title={title}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
autoFocus
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
// Don't show dropdown if there is already a value in the input field (when user is typing)
|
||||
setShowDropdown(!(newValue.length > 0));
|
||||
onChange(newValue);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onFocus={() => {
|
||||
// Don't show dropdown if there is already a value in the input field
|
||||
// or isInputFocused is undefined which means component is mounting
|
||||
setShowDropdown(!(value.length > 0) && isInputFocused !== undefined);
|
||||
|
||||
setIsInputFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsInputFocused(false);
|
||||
}}
|
||||
contentAfter={
|
||||
value.length > 0 ? (
|
||||
<Button
|
||||
aria-label="Clear filter"
|
||||
className={styles.inputButton}
|
||||
size="small"
|
||||
icon={<DismissRegular />}
|
||||
onClick={() => {
|
||||
onChange("");
|
||||
setIsInputFocused(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label="Open dropdown"
|
||||
className={styles.inputButton}
|
||||
size="small"
|
||||
icon={<ArrowDownRegular />}
|
||||
onClick={() => {
|
||||
setShowDropdown(true);
|
||||
setAutofocusFirstDropdownItem(true);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Popover
|
||||
inline
|
||||
unstable_disableAutoFocus
|
||||
// trapFocus
|
||||
open={showDropdown}
|
||||
onOpenChange={handleOpenChange}
|
||||
positioning={{ positioningRef, position: "below", align: "start", offset: 4 }}
|
||||
>
|
||||
<PopoverSurface className={styles.container}>
|
||||
{dropdownOptions.map((section, sectionIndex) => (
|
||||
<div key={section.label}>
|
||||
<div className={styles.dropdownHeader} style={{ color: theme.palette.themePrimary }}>
|
||||
{section.label}
|
||||
</div>
|
||||
<div className={styles.dropdownStack}>
|
||||
{section.options.map((option, index) => (
|
||||
<Button
|
||||
key={option}
|
||||
ref={(el) => (itemRefs.current[indexMap.get(`${sectionIndex}-${index}`)] = el)}
|
||||
appearance="transparent"
|
||||
shape="square"
|
||||
className={styles.dropdownOption}
|
||||
onClick={() => {
|
||||
onChange(option);
|
||||
setShowDropdown(false);
|
||||
setIsInputFocused(true);
|
||||
}}
|
||||
onBlur={() =>
|
||||
!bottomLink &&
|
||||
sectionIndex === dropdownOptions.length - 1 &&
|
||||
index === section.options.length - 1 &&
|
||||
setShowDropdown(false)
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) =>
|
||||
handleDownDropdownItemKeyDown(e, indexMap.get(`${sectionIndex}-${index}`))
|
||||
}
|
||||
>
|
||||
{option}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{bottomLink && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className={styles.bottomSection}>
|
||||
<Link
|
||||
ref={(el) => (itemRefs.current[flatIndex] = el)}
|
||||
href={bottomLink.url}
|
||||
target="_blank"
|
||||
onBlur={() => setShowDropdown(false)}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLAnchorElement>) => handleDownDropdownItemKeyDown(e, flatIndex)}
|
||||
>
|
||||
{bottomLink.text}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PopoverSurface>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
ComputedPropertiesComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||
import {
|
||||
ContainerVectorPolicyComponent,
|
||||
ContainerVectorPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
||||
ContainerPolicyComponent,
|
||||
ContainerPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -105,6 +105,13 @@ export interface SettingsComponentState {
|
||||
isSubSettingsSaveable: boolean;
|
||||
isSubSettingsDiscardable: boolean;
|
||||
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||
fullTextPolicy: DataModels.FullTextPolicy;
|
||||
fullTextPolicyBaseline: DataModels.FullTextPolicy;
|
||||
shouldDiscardContainerPolicies: boolean;
|
||||
isContainerPolicyDirty: boolean;
|
||||
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
shouldDiscardIndexingPolicy: boolean;
|
||||
@@ -149,6 +156,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
private shouldShowPartitionKeyEditor: boolean;
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private isFullTextSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
@@ -164,6 +172,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
|
||||
@@ -203,6 +212,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
|
||||
vectorEmbeddingPolicy: undefined,
|
||||
vectorEmbeddingPolicyBaseline: undefined,
|
||||
fullTextPolicy: undefined,
|
||||
fullTextPolicyBaseline: undefined,
|
||||
shouldDiscardContainerPolicies: false,
|
||||
isContainerPolicyDirty: false,
|
||||
|
||||
indexingPolicyContent: undefined,
|
||||
indexingPolicyContentBaseline: undefined,
|
||||
shouldDiscardIndexingPolicy: false,
|
||||
@@ -307,6 +323,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return (
|
||||
this.state.isScaleSaveable ||
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
@@ -318,6 +335,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return (
|
||||
this.state.isScaleDiscardable ||
|
||||
this.state.isSubSettingsDiscardable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
@@ -405,6 +423,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
||||
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicyBaseline,
|
||||
fullTextPolicy: this.state.fullTextPolicyBaseline,
|
||||
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
||||
indexesToAdd: [],
|
||||
indexesToDrop: [],
|
||||
@@ -416,11 +436,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
||||
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
||||
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
||||
shouldDiscardContainerPolicies: true,
|
||||
shouldDiscardIndexingPolicy: true,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isMongoIndexingPolicySaveable: false,
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
@@ -448,9 +470,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
||||
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
||||
|
||||
private onVectorEmbeddingPolicyChange = (newVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy): void =>
|
||||
this.setState({ vectorEmbeddingPolicy: newVectorEmbeddingPolicy });
|
||||
|
||||
private onFullTextPolicyChange = (newFullTextPolicy: DataModels.FullTextPolicy): void =>
|
||||
this.setState({ fullTextPolicy: newFullTextPolicy });
|
||||
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||
|
||||
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
||||
|
||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||
|
||||
private logIndexingPolicySuccessMessage = (): void => {
|
||||
@@ -538,6 +568,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
||||
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
||||
|
||||
private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void =>
|
||||
this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty });
|
||||
|
||||
private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void =>
|
||||
this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty });
|
||||
|
||||
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
||||
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
||||
|
||||
@@ -691,6 +727,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
const vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy =
|
||||
this.collection.vectorEmbeddingPolicy && this.collection.vectorEmbeddingPolicy();
|
||||
const fullTextPolicy: DataModels.FullTextPolicy =
|
||||
this.collection.fullTextPolicy && this.collection.fullTextPolicy();
|
||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
@@ -724,6 +764,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
||||
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
|
||||
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
|
||||
fullTextPolicy: fullTextPolicy,
|
||||
fullTextPolicyBaseline: fullTextPolicy,
|
||||
indexingPolicyContent: indexingPolicyContent,
|
||||
indexingPolicyContentBaseline: indexingPolicyContent,
|
||||
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
||||
@@ -854,6 +898,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
if (
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty
|
||||
@@ -875,6 +920,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||
newCollection.defaultTtl = defaultTtl;
|
||||
|
||||
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
|
||||
|
||||
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
||||
|
||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||
|
||||
newCollection.changeFeedPolicy =
|
||||
@@ -913,6 +962,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
|
||||
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
|
||||
|
||||
if (wasIndexingPolicyModified) {
|
||||
await this.refreshIndexTransformationProgress();
|
||||
@@ -921,6 +972,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false,
|
||||
isComputedPropertiesDirty: false,
|
||||
@@ -1091,6 +1143,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
||||
};
|
||||
|
||||
const containerPolicyComponentProps: ContainerPolicyComponentProps = {
|
||||
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicy,
|
||||
vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline,
|
||||
onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange,
|
||||
onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange,
|
||||
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
||||
fullTextPolicy: this.state.fullTextPolicy,
|
||||
fullTextPolicyBaseline: this.state.fullTextPolicyBaseline,
|
||||
onFullTextPolicyChange: this.onFullTextPolicyChange,
|
||||
onFullTextPolicyDirtyChange: this.onFullTextPolicyDirtyChange,
|
||||
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
||||
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
||||
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
||||
};
|
||||
|
||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||
@@ -1148,10 +1215,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
explorer: this.props.settingsTab.getContainer(),
|
||||
};
|
||||
|
||||
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
||||
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
||||
};
|
||||
|
||||
const tabs: SettingsV2TabInfo[] = [];
|
||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||
tabs.push({
|
||||
@@ -1165,10 +1228,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||
});
|
||||
|
||||
if (this.isVectorSearchEnabled) {
|
||||
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
||||
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||
/>
|
||||
{this.props.isVectorSearchEnabled && (
|
||||
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
||||
Container vector policies and vector indexes are not modifiable after container creation
|
||||
</MessageBar>
|
||||
)}
|
||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||
import {
|
||||
@@ -177,12 +178,14 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
||||
To change the partition key, a new destination container must be created or an existing destination container
|
||||
selected. Data will then be copied to the destination container.
|
||||
</Text>
|
||||
<PrimaryButton
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
{configContext.platform !== Platform.Emulator && (
|
||||
<PrimaryButton
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
)}
|
||||
{portalDataTransferJob && (
|
||||
<Stack>
|
||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||
|
||||
@@ -17,14 +17,13 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
PriceBreakdown,
|
||||
@@ -366,29 +365,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
});
|
||||
};
|
||||
|
||||
private minRUperGBSurvey = (): JSX.Element => {
|
||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||
const oneTBinKB = 1000000000;
|
||||
const minRUperGB = 10;
|
||||
const featureFlagEnabled = userContext.features.showMinRUSurvey;
|
||||
const collectionIsEligible =
|
||||
userContext.subscriptionType !== SubscriptionType.Internal &&
|
||||
this.props.usageSizeInKB > oneTBinKB &&
|
||||
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
||||
if (featureFlagEnabled || collectionIsEligible) {
|
||||
return (
|
||||
<Text>
|
||||
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
||||
<a target="_blank" rel="noreferrer" href={href}>
|
||||
this questionnaire
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private renderThroughputModeChoices = (): JSX.Element => {
|
||||
const labelId = "settingsV2RadioButtonLabelId";
|
||||
return (
|
||||
@@ -661,7 +637,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
|
||||
@@ -4,7 +4,14 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
||||
export type isDirtyTypes =
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
| DataModels.IndexingPolicy
|
||||
| DataModels.ComputedProperties
|
||||
| DataModels.VectorEmbedding[]
|
||||
| DataModels.FullTextPolicy;
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
@@ -50,6 +57,11 @@ export enum SettingsV2TabTypes {
|
||||
ContainerVectorPolicyTab,
|
||||
}
|
||||
|
||||
export enum ContainerPolicyTabTypes {
|
||||
VectorPolicyTab,
|
||||
FullTextPolicyTab,
|
||||
}
|
||||
|
||||
export interface IsComponentDirtyResult {
|
||||
isSaveable: boolean;
|
||||
isDiscardable: boolean;
|
||||
@@ -154,7 +166,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||
return "Computed Properties";
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return "Container Vector Policy (preview)";
|
||||
return "Container Policies";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ export const collection = {
|
||||
query: "query",
|
||||
},
|
||||
]),
|
||||
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
||||
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
||||
readSettings: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
@@ -71,6 +72,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
isAutoPilotSelected={false}
|
||||
@@ -132,6 +134,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
@@ -148,6 +151,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
displayedTtlSeconds="5"
|
||||
@@ -249,6 +253,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
@@ -265,6 +270,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
explorer={
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
@@ -34,10 +35,15 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
setIsThroughputCapExceeded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
const defaultThroughput: number =
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
|
||||
);
|
||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@@ -47,7 +53,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
|
||||
useEffect(() => {
|
||||
// throughput cap check for the initial state
|
||||
let totalThroughput = 0;
|
||||
@@ -157,9 +162,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(defaultThroughput);
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useCallback } from "react";
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
onClick: (value?: React.RefObject<HTMLElement>) => void;
|
||||
iconSrc?: string;
|
||||
isDisabled?: boolean;
|
||||
styleClass?: string;
|
||||
@@ -74,6 +74,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
openItems,
|
||||
}: TreeNodeComponentProps): JSX.Element => {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
|
||||
const treeStyles = useTreeStyles();
|
||||
|
||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
||||
@@ -141,7 +142,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
||||
disabled={menuItem.isDisabled}
|
||||
key={menuItem.label}
|
||||
onClick={menuItem.onClick}
|
||||
onClick={() => menuItem.onClick(contextMenuRef)}
|
||||
>
|
||||
{menuItem.label}
|
||||
</MenuItem>
|
||||
@@ -190,6 +191,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
||||
data-test="TreeNode/ContextMenuTrigger"
|
||||
appearance="subtle"
|
||||
ref={contextMenuRef}
|
||||
icon={<MoreHorizontal20Regular />}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
|
||||
@@ -1478,14 +1478,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||
onClick={[MockFunction enabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
enabledItem
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||
disabled={true}
|
||||
onClick={[MockFunction disabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
disabledItem
|
||||
</MenuItem>
|
||||
@@ -1518,7 +1518,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
<MenuItem
|
||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||
key="enabledItem"
|
||||
onClick={[MockFunction enabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
enabledItem
|
||||
</MenuItem>
|
||||
@@ -1526,7 +1526,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||
disabled={true}
|
||||
key="disabledItem"
|
||||
onClick={[MockFunction disabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
disabledItem
|
||||
</MenuItem>
|
||||
|
||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
|
||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import React from "react";
|
||||
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
||||
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
|
||||
|
||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component = render(
|
||||
<AddVectorEmbeddingPolicyForm
|
||||
vectorEmbedding={mockVectorEmbedding}
|
||||
vectorIndex={mockVectorIndex}
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddings={mockVectorEmbedding}
|
||||
vectorIndexes={mockVectorIndex}
|
||||
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||
/>,
|
||||
);
|
||||
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
});
|
||||
|
||||
test("calls onDelete when delete button is clicked", async () => {
|
||||
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
||||
const deleteButton = component.container.querySelector("#delete-Vector-embedding-1");
|
||||
fireEvent.click(deleteButton);
|
||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||
@@ -49,21 +49,19 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
|
||||
test("validates input correctly", async () => {
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
||||
await waitFor(() => expect(screen.getByText("Path should not be empty")).toBeInTheDocument(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
||||
).toBeInTheDocument(),
|
||||
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(),
|
||||
{
|
||||
timeout: 1500,
|
||||
},
|
||||
);
|
||||
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
||||
await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
await waitFor(
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -35,7 +35,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
@@ -259,25 +259,8 @@ export default class Explorer {
|
||||
|
||||
public async openLoginForEntraIDPopUp(): Promise<void> {
|
||||
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||
/\/$/,
|
||||
"/.default",
|
||||
);
|
||||
const msalInstance = await getMsalInstance();
|
||||
|
||||
try {
|
||||
const response = await msalInstance.loginPopup({
|
||||
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")}`,
|
||||
});
|
||||
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false);
|
||||
updateUserContext({ aadToken: aadToken });
|
||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||
} catch (error) {
|
||||
@@ -295,37 +278,6 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openNPSSurveyDialog(): void {
|
||||
if (!Platform.Portal || !["Postgres", "SQL", "Mongo"].includes(userContext.apiType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
const SEVEN_DAYS_IN_MS = 604800000;
|
||||
|
||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
||||
Logger.logInfo(
|
||||
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
|
||||
"Explorer/openNPSSurveyDialog",
|
||||
);
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
}
|
||||
} else {
|
||||
// Show survey when an existing account is older than 7 days
|
||||
if (
|
||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
||||
) {
|
||||
Logger.logInfo(
|
||||
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
||||
"Explorer/openNPSSurveyDialog",
|
||||
);
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||
Logger.logInfo(
|
||||
@@ -1151,7 +1103,7 @@ export default class Explorer {
|
||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
@@ -56,6 +57,19 @@ function openCollectionTab(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
!(
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||
(
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||
)
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//expand database first if not expanded to load the collections
|
||||
if (!database.isDatabaseExpanded?.()) {
|
||||
database.expandDatabase?.();
|
||||
@@ -121,10 +135,28 @@ function openCollectionTab(
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||
openQueryTabAction.splitterDirection,
|
||||
openQueryTabAction.queryViewSizePercent,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
|
||||
) {
|
||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||
collection.onNewMongoQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||
openQueryTabAction.splitterDirection,
|
||||
openQueryTabAction.queryViewSizePercent,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,15 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||
import {
|
||||
FullTextPoliciesComponent,
|
||||
getFullTextLanguageOptions,
|
||||
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
@@ -30,7 +34,12 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import {
|
||||
isCapabilityEnabled,
|
||||
isFullTextSearchEnabled,
|
||||
isServerlessAccount,
|
||||
isVectorSearchEnabled,
|
||||
} from "Utils/CapabilityUtils";
|
||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
@@ -109,6 +118,9 @@ export interface AddCollectionPanelState {
|
||||
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||
vectorPolicyValidated: boolean;
|
||||
fullTextPolicy: DataModels.FullTextPolicy;
|
||||
fullTextIndexes: DataModels.FullTextIndex[];
|
||||
fullTextPolicyValidated: boolean;
|
||||
}
|
||||
|
||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||
@@ -125,7 +137,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
createNewDatabase:
|
||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||
isSharedThroughputChecked: getNewDatabaseSharedThroughputDefault(),
|
||||
selectedDatabaseId:
|
||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||
@@ -147,6 +159,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
vectorEmbeddingPolicy: [],
|
||||
vectorIndexingPolicy: [],
|
||||
vectorPolicyValidated: true,
|
||||
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
|
||||
fullTextIndexes: [],
|
||||
fullTextPolicyValidated: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -804,22 +819,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{this.shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical store
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{this.getAnalyticalStorageContent()}
|
||||
</Text>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
@@ -863,6 +865,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
@@ -890,9 +893,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<AddVectorEmbeddingPolicyForm
|
||||
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
||||
vectorIndex={this.state.vectorIndexingPolicy}
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddings={this.state.vectorEmbeddingPolicy}
|
||||
vectorIndexes={this.state.vectorIndexingPolicy}
|
||||
onVectorEmbeddingChange={(
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||
@@ -906,6 +909,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
)}
|
||||
{this.shouldShowFullTextSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Full Text Search Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
this.scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
}}
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<FullTextPoliciesComponent
|
||||
fullTextPolicy={this.state.fullTextPolicy}
|
||||
onFullTextPathChange={(
|
||||
fullTextPolicy: DataModels.FullTextPolicy,
|
||||
fullTextIndexes: DataModels.FullTextIndex[],
|
||||
fullTextPolicyValidated: boolean,
|
||||
) => {
|
||||
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
)}
|
||||
{userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
@@ -1138,10 +1169,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||
}
|
||||
|
||||
private getSharedThroughputDefault(): boolean {
|
||||
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
|
||||
}
|
||||
|
||||
private getFreeTierIndexingText(): string {
|
||||
return this.state.enableIndexing
|
||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||
@@ -1191,12 +1218,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return "";
|
||||
}
|
||||
|
||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||
private getAnalyticalStorageContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||
impacting the performance of transactional workloads.{" "}
|
||||
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
||||
<Link
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||
target="_blank"
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
@@ -1215,6 +1246,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
||||
// return (
|
||||
// <Text variant="small">
|
||||
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
// magna aliqua.{" "}
|
||||
// <Link target="_blank" href="https://aka.ms/CosmosFullTextSearch">
|
||||
// Learn more
|
||||
// </Link>
|
||||
// </Text>
|
||||
// );
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
@@ -1278,6 +1322,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
}
|
||||
|
||||
private shouldShowFullTextSearchParameters() {
|
||||
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
}
|
||||
|
||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||
if (this.state.uniqueKeys?.length === 0) {
|
||||
return undefined;
|
||||
@@ -1334,9 +1382,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
return false;
|
||||
if (this.shouldShowVectorSearchParameters()) {
|
||||
if (!this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.state.fullTextPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1427,6 +1482,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.shouldShowFullTextSearchParameters()) {
|
||||
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
|
||||
}
|
||||
|
||||
const telemetryData = {
|
||||
database: {
|
||||
id: databaseId,
|
||||
@@ -1486,6 +1545,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
uniqueKeyPolicy,
|
||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||
vectorEmbeddingPolicy,
|
||||
fullTextPolicy: this.state.fullTextPolicy,
|
||||
};
|
||||
|
||||
this.setState({ isExecuting: true });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
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 [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
||||
subscriptionType !== SubscriptionType.EA && !isServerlessAccount(),
|
||||
getNewDatabaseSharedThroughputDefault(),
|
||||
);
|
||||
const [formErrors, setFormErrors] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
|
||||
@@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={true}
|
||||
checked={false}
|
||||
label="Provision throughput"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
@@ -90,14 +90,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
`;
|
||||
|
||||
@@ -124,7 +124,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
message:
|
||||
"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()}?`;
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
@@ -132,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
|
||||
<Text variant="small">{confirmDatabase}</Text>
|
||||
<TextField
|
||||
id="confirmDatabaseId"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AuthError as msalAuthError,
|
||||
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
|
||||
} from "@azure/msal-browser";
|
||||
import {
|
||||
Checkbox,
|
||||
ChoiceGroup,
|
||||
@@ -5,8 +9,6 @@ import {
|
||||
IChoiceGroupOption,
|
||||
ISpinButtonStyles,
|
||||
IToggleStyles,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Position,
|
||||
SpinButton,
|
||||
Toggle,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
} from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
@@ -108,7 +111,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
||||
: Constants.RBACOptions.setAutomaticRBACOption,
|
||||
);
|
||||
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
|
||||
|
||||
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||
@@ -172,15 +174,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const styles = useStyles();
|
||||
|
||||
const explorerVersion = configContext.gitSha;
|
||||
const isEmulator = configContext.platform === Platform.Emulator;
|
||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||
const showRetrySettings =
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") &&
|
||||
!isEmulator;
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const showEnableEntraIdRbac =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
!isEmulator;
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
||||
const shouldShowCopilotSampleDBOption =
|
||||
userContext.apiType === "SQL" &&
|
||||
useQueryCopilot.getState().copilotEnabled &&
|
||||
useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
!isEmulator;
|
||||
|
||||
const handlerOnSubmit = async () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
@@ -191,6 +204,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||
|
||||
if (
|
||||
enableDataPlaneRBACOption !== LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ||
|
||||
retryAttempts !== LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ||
|
||||
retryInterval !== LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval) ||
|
||||
MaxWaitTimeInSeconds !== LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
|
||||
) {
|
||||
updateUserContext({
|
||||
refreshCosmosClient: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||
if (
|
||||
@@ -200,13 +224,29 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
) {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true,
|
||||
hasDataPlaneRbacSettingChanged: 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 {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: false,
|
||||
hasDataPlaneRbacSettingChanged: true,
|
||||
});
|
||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
||||
@@ -347,13 +387,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
option: IChoiceGroupOption,
|
||||
): void => {
|
||||
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 => {
|
||||
@@ -469,7 +502,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
return (
|
||||
<RightPaneForm {...genericPaneProps}>
|
||||
<div className={`paneMainContent ${styles.container}`}>
|
||||
<Accordion className={styles.firstItem}>
|
||||
<Accordion className={`customAccordion ${styles.firstItem}`}>
|
||||
{shouldShowQueryPageOptions && (
|
||||
<AccordionItem value="1">
|
||||
<AccordionHeader>
|
||||
@@ -519,51 +552,37 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<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}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
{showEnableEntraIdRbac && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
|
||||
RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" && !isEmulator && (
|
||||
<>
|
||||
<AccordionItem value="3">
|
||||
<AccordionHeader>
|
||||
@@ -661,102 +680,103 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
{showRetrySettings && (
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
|
||||
as part of the response. Default value is 0 milliseconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
</InfoTooltip>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{!isEmulator && (
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
/>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
||||
part of the response. Default value is 0 milliseconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<AccordionItem value="8">
|
||||
<AccordionHeader>
|
||||
@@ -782,7 +802,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowParallelismOption && (
|
||||
<AccordionItem value="9">
|
||||
<AccordionHeader>
|
||||
@@ -816,7 +835,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowPriorityLevelOption && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
@@ -840,7 +858,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowGraphAutoVizOption && (
|
||||
<AccordionItem value="11">
|
||||
<AccordionHeader>
|
||||
@@ -862,7 +879,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowCopilotSampleDBOption && (
|
||||
<AccordionItem value="12">
|
||||
<AccordionHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||
>
|
||||
<Accordion
|
||||
className="___1uf6361_0000000 fz7g6wx"
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
>
|
||||
<AccordionItem
|
||||
value="1"
|
||||
@@ -572,7 +572,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||
>
|
||||
<Accordion
|
||||
className="___1uf6361_0000000 fz7g6wx"
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
>
|
||||
<AccordionItem
|
||||
value="6"
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={true}
|
||||
checked={false}
|
||||
label="Share throughput across containers"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
@@ -137,14 +137,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
@@ -263,6 +255,14 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -309,40 +309,24 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Analytical store
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about analytical store."
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content={
|
||||
<Text
|
||||
variant="small"
|
||||
>
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
}
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
@@ -400,6 +384,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about Azure Synapse Link."
|
||||
className="capacitycalculator-link"
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
|
||||
@@ -361,13 +361,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<span
|
||||
className="css-113"
|
||||
>
|
||||
Confirm by typing the
|
||||
Database
|
||||
id
|
||||
Confirm by typing the Database id (name)
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Confirm by typing the Database id"
|
||||
ariaLabel="Confirm by typing the Database id (name)"
|
||||
autoFocus={true}
|
||||
data-test="Input:confirmDatabaseId"
|
||||
id="confirmDatabaseId"
|
||||
@@ -382,7 +380,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the Database id"
|
||||
ariaLabel="Confirm by typing the Database id (name)"
|
||||
autoFocus={true}
|
||||
data-test="Input:confirmDatabaseId"
|
||||
deferredValidationTime={200}
|
||||
@@ -677,7 +675,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Confirm by typing the Database id"
|
||||
aria-label="Confirm by typing the Database id (name)"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-117"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
|
||||
@@ -79,9 +79,13 @@ export const QueryCopilotFeedbackModal = ({
|
||||
readOnly
|
||||
/>
|
||||
<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
|
||||
</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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
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
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
|
||||
@@ -75,6 +75,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
const inputEdited = useRef(false);
|
||||
const itemRefs = useRef([]);
|
||||
const searchInputRef = useRef(null);
|
||||
const copyQueryRef = useRef(null);
|
||||
const {
|
||||
openFeedbackModal,
|
||||
hideFeedbackModalForLikedQueries,
|
||||
@@ -132,6 +133,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
document.body.removeChild(queryElement);
|
||||
|
||||
setshowCopyPopup(true);
|
||||
copyQueryRef.current.focus();
|
||||
setTimeout(() => {
|
||||
setshowCopyPopup(false);
|
||||
}, 6000);
|
||||
@@ -305,7 +307,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
if (isGeneratingQuery === null) {
|
||||
return " ";
|
||||
} else if (isGeneratingQuery) {
|
||||
return "Content is loading";
|
||||
return "Thinking";
|
||||
} else {
|
||||
return "Content is updated";
|
||||
}
|
||||
@@ -400,6 +402,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
allowDisabledFocus={true}
|
||||
style={{ background: "none" }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
aria-label="Send"
|
||||
@@ -676,6 +679,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
)}
|
||||
<CommandBarButton
|
||||
className="copyQuery"
|
||||
elementRef={copyQueryRef}
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||
@@ -706,6 +710,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{(showFeedbackBar || isGeneratingQuery) && (
|
||||
<span role="alert" className="screenReaderOnly" aria-label={getAriaLabel()} />
|
||||
)}
|
||||
{isGeneratingQuery && (
|
||||
<ProgressIndicator
|
||||
label="Thinking..."
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { Allotment, AllotmentHandle } from "allotment";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
const useSidebarStyles = makeStyles({
|
||||
sidebar: {
|
||||
@@ -109,6 +109,7 @@ interface GlobalCommand {
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
keyboardAction?: KeyboardAction;
|
||||
ref?: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
@@ -118,6 +119,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
||||
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
||||
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||
const primaryFocusableRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const actions = useMemo<GlobalCommand[]>(() => {
|
||||
if (
|
||||
@@ -177,6 +179,16 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
);
|
||||
}, [actions, setKeyboardActions]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (primaryFocusableRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
primaryFocusableRef.current.focus();
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
if (!primaryAction) {
|
||||
return null;
|
||||
}
|
||||
@@ -184,7 +196,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
return (
|
||||
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
||||
{actions.length === 1 ? (
|
||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick}>
|
||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -194,7 +206,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
<div ref={setGlobalCommandButton}>
|
||||
<SplitButton
|
||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }}
|
||||
className={styles.globalCommandsSplitButton}
|
||||
icon={primaryAction.icon}
|
||||
>
|
||||
@@ -282,67 +294,69 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{hasSidebar && (
|
||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||
<div className={styles.sidebarContainer}>
|
||||
{loading && (
|
||||
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
||||
// https://github.com/microsoft/fluentui/issues/29076
|
||||
<div className={styles.loadingProgressBar} title="Refreshing tree..." />
|
||||
)}
|
||||
{expanded ? (
|
||||
<>
|
||||
<div className={styles.floatingControlsContainer}>
|
||||
<div className={styles.floatingControls}>
|
||||
<button
|
||||
type="button"
|
||||
data-test="Sidebar/RefreshButton"
|
||||
className={styles.floatingControlButton}
|
||||
disabled={loading}
|
||||
title="Refresh"
|
||||
onClick={onRefreshClick}
|
||||
>
|
||||
<ArrowSync12Regular />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.floatingControlButton}
|
||||
title="Collapse sidebar"
|
||||
onClick={() => collapse()}
|
||||
>
|
||||
<ChevronLeft12Regular />
|
||||
</button>
|
||||
<div className="sidebarContainer">
|
||||
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{hasSidebar && (
|
||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||
<div className={styles.sidebarContainer}>
|
||||
{loading && (
|
||||
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
||||
// https://github.com/microsoft/fluentui/issues/29076
|
||||
<div className={styles.loadingProgressBar} title="Refreshing tree..." />
|
||||
)}
|
||||
{expanded ? (
|
||||
<>
|
||||
<div className={styles.floatingControlsContainer}>
|
||||
<div className={styles.floatingControls}>
|
||||
<button
|
||||
type="button"
|
||||
data-test="Sidebar/RefreshButton"
|
||||
className={styles.floatingControlButton}
|
||||
disabled={loading}
|
||||
title="Refresh"
|
||||
onClick={onRefreshClick}
|
||||
>
|
||||
<ArrowSync12Regular />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.floatingControlButton}
|
||||
title="Collapse sidebar"
|
||||
onClick={() => collapse()}
|
||||
>
|
||||
<ChevronLeft12Regular />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.expandedContent}
|
||||
style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}
|
||||
<div
|
||||
className={styles.expandedContent}
|
||||
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} />}
|
||||
<ResourceTree explorer={explorer} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.floatingControlButton}
|
||||
title="Expand sidebar"
|
||||
onClick={() => expand()}
|
||||
>
|
||||
<ChevronRight12Regular />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</CosmosFluentProvider>
|
||||
<ChevronRight12Regular />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</CosmosFluentProvider>
|
||||
</Allotment.Pane>
|
||||
)}
|
||||
<Allotment.Pane minSize={200}>
|
||||
<Tabs explorer={explorer} />
|
||||
</Allotment.Pane>
|
||||
)}
|
||||
<Allotment.Pane minSize={200}>
|
||||
<Tabs explorer={explorer} />
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
</Allotment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={imgSrc} />
|
||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
||||
</div>
|
||||
<Stack style={{ marginLeft: 16 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||
|
||||
@@ -757,6 +757,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
CassandraProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
|
||||
@@ -3,17 +3,11 @@
|
||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
deleteState,
|
||||
loadState,
|
||||
saveState,
|
||||
saveStateDebounced,
|
||||
deleteSubComponentState,
|
||||
readSubComponentState,
|
||||
saveSubComponentState,
|
||||
} from "Shared/AppStatePersistenceUtility";
|
||||
import { userContext } from "UserContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
const componentName = AppStateComponentNames.DocumentsTab;
|
||||
|
||||
export enum SubComponentName {
|
||||
ColumnSizes = "ColumnSizes",
|
||||
@@ -21,6 +15,7 @@ export enum SubComponentName {
|
||||
MainTabDivider = "MainTabDivider",
|
||||
ColumnsSelection = "ColumnsSelection",
|
||||
ColumnSort = "ColumnSort",
|
||||
CurrentFilter = "CurrentFilter",
|
||||
}
|
||||
|
||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||
@@ -30,84 +25,22 @@ export type TabDivider = { leftPaneWidthPercent: number };
|
||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param defaultValue Will be returned if persisted state is not found
|
||||
* @returns
|
||||
*/
|
||||
export const readSubComponentState = <T>(
|
||||
// Wrap the ...SubComponentState functions for type safety
|
||||
|
||||
export const readDocumentsTabSubComponentState = <T>(
|
||||
subComponentName: SubComponentName,
|
||||
collection: ViewModels.CollectionBase,
|
||||
defaultValue: T,
|
||||
): T => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
||||
return defaultValue;
|
||||
}
|
||||
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
|
||||
|
||||
const state = loadState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
}) as T;
|
||||
|
||||
return state || defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param state State to save
|
||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
||||
*/
|
||||
export const saveSubComponentState = <T>(
|
||||
export const saveDocumentsTabSubComponentState = <T>(
|
||||
subComponentName: SubComponentName,
|
||||
collection: ViewModels.CollectionBase,
|
||||
state: T,
|
||||
debounce?: boolean,
|
||||
): void => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
|
||||
|
||||
(debounce ? saveStateDebounced : saveState)(
|
||||
{
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
},
|
||||
state,
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
deleteState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
});
|
||||
};
|
||||
export const deleteDocumentsTabSubComponentState = (
|
||||
subComponentName: SubComponentName,
|
||||
collection: ViewModels.CollectionBase,
|
||||
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);
|
||||
|
||||
@@ -49,6 +49,7 @@ jest.mock("Common/dataAccess/queryDocuments", () => ({
|
||||
requestCharge: 1,
|
||||
activityId: "activityId",
|
||||
indexMetrics: "indexMetrics",
|
||||
correlatedActivityId: undefined,
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
@@ -385,22 +386,6 @@ describe("Documents tab (noSql API)", () => {
|
||||
it("should render the page", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("clicking on Edit filter should render the Apply Filter button", () => {
|
||||
wrapper
|
||||
.findWhere((node) => node.text() === "Edit Filter")
|
||||
.at(0)
|
||||
.simulate("click");
|
||||
expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking on Edit filter should render input for filter", () => {
|
||||
wrapper
|
||||
.findWhere((node) => node.text() === "Edit Filter")
|
||||
.at(0)
|
||||
.simulate("click");
|
||||
expect(wrapper.find("Input.filterInput").exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Command bar buttons", () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
@@ -10,8 +9,7 @@ import {
|
||||
makeStyles,
|
||||
shorthands,
|
||||
} from "@fluentui/react-components";
|
||||
import { Dismiss16Filled } from "@fluentui/react-icons";
|
||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import MongoUtility from "Common/MongoUtility";
|
||||
import { createDocument } from "Common/dataAccess/createDocument";
|
||||
@@ -23,9 +21,11 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "Common/dataAccess/readDocument";
|
||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -35,8 +35,9 @@ import {
|
||||
FilterHistory,
|
||||
SubComponentName,
|
||||
TabDivider,
|
||||
readSubComponentState,
|
||||
saveSubComponentState,
|
||||
deleteDocumentsTabSubComponentState,
|
||||
readDocumentsTabSubComponentState,
|
||||
saveDocumentsTabSubComponentState,
|
||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
@@ -74,6 +75,7 @@ const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we
|
||||
const NO_SQL_THROTTLING_DOC_URL =
|
||||
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
||||
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
||||
const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer";
|
||||
|
||||
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||
export const useDocumentsTabStyles = makeStyles({
|
||||
@@ -90,12 +92,6 @@ export const useDocumentsTabStyles = makeStyles({
|
||||
alignItems: "center",
|
||||
...cosmosShorthands.borderBottom(),
|
||||
},
|
||||
filterInput: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
appliedFilter: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
tableContainer: {
|
||||
marginRight: tokens.spacingHorizontalXXXL,
|
||||
},
|
||||
@@ -146,6 +142,8 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
private title: string;
|
||||
private resourceTokenPartitionKey: string;
|
||||
|
||||
protected persistedState: OpenCollectionTab;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
|
||||
@@ -153,6 +151,13 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
this.title = options.title;
|
||||
this.partitionKey = options.partitionKey;
|
||||
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: options.isPreferredApiMongoDB ? TabKind.MongoDocuments : TabKind.SQLDocuments,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -556,8 +561,6 @@ export interface IDocumentsTabComponentProps {
|
||||
isTabActive: boolean;
|
||||
}
|
||||
|
||||
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
||||
|
||||
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
||||
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
|
||||
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
||||
@@ -583,14 +586,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onIsExecutingChange,
|
||||
isTabActive,
|
||||
}): JSX.Element => {
|
||||
const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true);
|
||||
const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false);
|
||||
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
||||
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
||||
const [filterContent, setFilterContent] = useState<string>("");
|
||||
const [filterContent, setFilterContent] = useState<string>(() =>
|
||||
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
|
||||
);
|
||||
|
||||
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
const filterInput = useRef<HTMLInputElement>(null);
|
||||
const styles = useDocumentsTabStyles();
|
||||
|
||||
// Query
|
||||
@@ -610,7 +611,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// Table user clicked on this row
|
||||
const [clickedRowIndex, setClickedRowIndex] = useState<number>(RESET_INDEX);
|
||||
// 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
|
||||
const [editorState, setEditorState] = useState<ViewModels.DocumentExplorerState>(
|
||||
@@ -619,7 +620,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// State
|
||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||
leftPaneWidthPercent: 35,
|
||||
}),
|
||||
);
|
||||
@@ -634,7 +635,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// User's filter history
|
||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||
);
|
||||
|
||||
// For progress bar for bulk delete (noSql)
|
||||
@@ -657,29 +658,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFilterFocused) {
|
||||
filterInput.current?.focus();
|
||||
}
|
||||
}, [isFilterFocused]);
|
||||
|
||||
// Clicked row must be defined
|
||||
useEffect(() => {
|
||||
if (documentIds.length > 0) {
|
||||
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]);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -773,11 +751,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}, timeout);
|
||||
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
||||
|
||||
const applyFilterButton = {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
const partitionKey: DataModels.PartitionKey = useMemo(
|
||||
() => _partitionKey || (_collection && _collection.partitionKey),
|
||||
[_collection, _partitionKey],
|
||||
@@ -804,7 +777,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
};
|
||||
|
||||
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||
SubComponentName.ColumnsSelection,
|
||||
_collection,
|
||||
undefined,
|
||||
@@ -848,12 +821,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// This is executed in onActivate() in the original code.
|
||||
useEffect(() => {
|
||||
setKeyboardActions({
|
||||
[KeyboardAction.SEARCH]: () => {
|
||||
onShowFilterClick();
|
||||
return true;
|
||||
},
|
||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||
setFilterContent("");
|
||||
updateFilterContent("");
|
||||
refreshDocumentsGrid(true);
|
||||
return true;
|
||||
},
|
||||
@@ -1334,12 +1303,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
],
|
||||
);
|
||||
|
||||
const onShowFilterClick = () => {
|
||||
setIsFilterCreated(true);
|
||||
setIsFilterExpanded(true);
|
||||
setIsFilterFocused(true);
|
||||
};
|
||||
|
||||
const queryTimeoutEnabled = useCallback(
|
||||
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||
[isPreferredApiMongoDB],
|
||||
@@ -1381,19 +1344,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
selectedColumnIds,
|
||||
]);
|
||||
|
||||
const onHideFilterClick = (): void => {
|
||||
setIsFilterExpanded(false);
|
||||
};
|
||||
|
||||
const onCloseButtonKeyDown: KeyboardEventHandler<HTMLSpanElement> = (event) => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
onHideFilterClick();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
||||
setDocumentIds(newDocumentsIds);
|
||||
|
||||
@@ -1535,14 +1485,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
};
|
||||
|
||||
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === Constants.NormalizedEventKey.Enter) {
|
||||
onApplyFilterClick();
|
||||
|
||||
// Suppress the default behavior of the key
|
||||
e.preventDefault();
|
||||
} else if (e.key === "Escape") {
|
||||
onHideFilterClick();
|
||||
|
||||
// Suppress the default behavior of the key
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -1714,7 +1659,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||
SubComponentName.ColumnsSelection,
|
||||
_collection,
|
||||
undefined,
|
||||
@@ -2025,7 +1970,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||
|
||||
setLastFilterContents(limitedLastFilterContents);
|
||||
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||
};
|
||||
|
||||
const refreshDocumentsGrid = useCallback(
|
||||
@@ -2040,10 +1985,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
applyFilterButtonPressed,
|
||||
});
|
||||
|
||||
// collapse filter
|
||||
setAppliedFilter(filterContent);
|
||||
setIsFilterExpanded(false);
|
||||
|
||||
// If apply filter is pressed, reset current selected document
|
||||
if (applyFilterButtonPressed) {
|
||||
setClickedRowIndex(RESET_INDEX);
|
||||
@@ -2086,7 +2027,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
setSelectedColumnIds(newSelectedColumnIds);
|
||||
|
||||
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||
selectedColumnIds: newSelectedColumnIds,
|
||||
columnDefinitions,
|
||||
});
|
||||
@@ -2115,174 +2056,143 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// 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("bulkdelete");
|
||||
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;
|
||||
};
|
||||
|
||||
const updateFilterContent = (filter: string): void => {
|
||||
if (filter === "" || filter === undefined) {
|
||||
deleteDocumentsTabSubComponentState(SubComponentName.CurrentFilter, _collection);
|
||||
} else {
|
||||
saveDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, filter, true);
|
||||
}
|
||||
setFilterContent(filter);
|
||||
};
|
||||
|
||||
return (
|
||||
<CosmosFluentProvider className={styles.container}>
|
||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||
{isFilterCreated && (
|
||||
<>
|
||||
{!isFilterExpanded && !isPreferredApiMongoDB && (
|
||||
<div className={styles.filterRow}>
|
||||
<span>SELECT * FROM c</span>
|
||||
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
||||
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
||||
Edit Filter
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isFilterExpanded && isPreferredApiMongoDB && (
|
||||
<div className={styles.filterRow}>
|
||||
{appliedFilter.length > 0 && <span>Filter :</span>}
|
||||
{!(appliedFilter.length > 0) && <span className="noFilterApplied">No filter applied</span>}
|
||||
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
||||
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
||||
Edit Filter
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isFilterExpanded && (
|
||||
<div className={styles.filterRow}>
|
||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||
<Input
|
||||
ref={filterInput}
|
||||
type="text"
|
||||
size="small"
|
||||
list={`filtersList-${getUniqueId(_collection)}`}
|
||||
className={`filterInput ${styles.filterInput}`}
|
||||
title="Type a query predicate or choose one from the list."
|
||||
placeholder={
|
||||
isPreferredApiMongoDB
|
||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||
}
|
||||
value={filterContent}
|
||||
autoFocus={true}
|
||||
onKeyDown={onFilterKeyDown}
|
||||
onChange={(e) => setFilterContent(e.target.value)}
|
||||
onBlur={() => setIsFilterFocused(false)}
|
||||
/>
|
||||
|
||||
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
||||
{addStringsNoDuplicate(
|
||||
lastFilterContents,
|
||||
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
||||
).map((filter) => (
|
||||
<option key={filter} value={filter} />
|
||||
))}
|
||||
</datalist>
|
||||
|
||||
<Button
|
||||
appearance="primary"
|
||||
size="small"
|
||||
onClick={onApplyFilterClick}
|
||||
disabled={!applyFilterButton.enabled}
|
||||
aria-label="Apply filter"
|
||||
tabIndex={0}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
{!isPreferredApiMongoDB && isExecuting && (
|
||||
<Button
|
||||
appearance="primary"
|
||||
size="small"
|
||||
aria-label="Cancel Query"
|
||||
onClick={() => queryAbortController.abort()}
|
||||
tabIndex={0}
|
||||
>
|
||||
Cancel Query
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
aria-label="close filter"
|
||||
tabIndex={0}
|
||||
onClick={onHideFilterClick}
|
||||
onKeyDown={onCloseButtonKeyDown}
|
||||
appearance="transparent"
|
||||
size="small"
|
||||
icon={<Dismiss16Filled />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* <Split> doesn't like to be a flex child */}
|
||||
<div style={{ overflow: "hidden", height: "100%" }}>
|
||||
<Allotment
|
||||
onDragEnd={(sizes: number[]) => {
|
||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||
setTabStateData(tabStateData);
|
||||
<div className={styles.filterRow}>
|
||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||
<InputDataList
|
||||
dropdownOptions={getFilterChoices()}
|
||||
placeholder={
|
||||
isPreferredApiMongoDB
|
||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||
}
|
||||
title="Type a query predicate or choose one from the list."
|
||||
value={filterContent}
|
||||
onChange={updateFilterContent}
|
||||
onKeyDown={onFilterKeyDown}
|
||||
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (isExecuting) {
|
||||
if (!isPreferredApiMongoDB) {
|
||||
queryAbortController.abort();
|
||||
}
|
||||
} else {
|
||||
onApplyFilterClick();
|
||||
}
|
||||
}}
|
||||
disabled={isExecuting && isPreferredApiMongoDB}
|
||||
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||
<div className={styles.tableContainer}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
height: "100%",
|
||||
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
||||
} /* Fix to make table not resize beyond parent's width */
|
||||
}
|
||||
>
|
||||
<DocumentsTableComponent
|
||||
onRefreshTable={() => refreshDocumentsGrid(false)}
|
||||
items={tableItems}
|
||||
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
selectedRows={selectedRows}
|
||||
size={tableContainerSizePx}
|
||||
selectedColumnIds={selectedColumnIds}
|
||||
columnDefinitions={columnDefinitions}
|
||||
isRowSelectionDisabled={
|
||||
isBulkDeleteDisabled ||
|
||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||
}
|
||||
onColumnSelectionChange={onColumnSelectionChange}
|
||||
defaultColumnSelection={getInitialColumnSelection()}
|
||||
collection={_collection}
|
||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{tableItems.length > 0 && (
|
||||
<a
|
||||
className={styles.loadMore}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
||||
onKeyDown={onLoadMoreKeyInput}
|
||||
>
|
||||
Load more
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
<Allotment.Pane minSize={30}>
|
||||
<div style={{ height: "100%", width: "100%" }}>
|
||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={selectedDocumentContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Document editor"}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={_onEditorContentChange}
|
||||
enableWordWrapContextMenuItem={true}
|
||||
/>
|
||||
)}
|
||||
{selectedRows.size > 1 && (
|
||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
||||
)}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
|
||||
</Button>
|
||||
</div>
|
||||
<Allotment
|
||||
onDragEnd={(sizes: number[]) => {
|
||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||
setTabStateData(tabStateData);
|
||||
}}
|
||||
>
|
||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||
<div className={styles.tableContainer}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
height: "100%",
|
||||
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
||||
} /* Fix to make table not resize beyond parent's width */
|
||||
}
|
||||
>
|
||||
<DocumentsTableComponent
|
||||
onRefreshTable={() => refreshDocumentsGrid(false)}
|
||||
items={tableItems}
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
selectedRows={selectedRows}
|
||||
size={tableContainerSizePx}
|
||||
selectedColumnIds={selectedColumnIds}
|
||||
columnDefinitions={columnDefinitions}
|
||||
isRowSelectionDisabled={
|
||||
isBulkDeleteDisabled ||
|
||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||
}
|
||||
onColumnSelectionChange={onColumnSelectionChange}
|
||||
defaultColumnSelection={getInitialColumnSelection()}
|
||||
collection={_collection}
|
||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{tableItems.length > 0 && (
|
||||
<a
|
||||
className={styles.loadMore}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
||||
onKeyDown={onLoadMoreKeyInput}
|
||||
>
|
||||
Load more
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
<Allotment.Pane minSize={30}>
|
||||
<div style={{ height: "100%", width: "100%" }}>
|
||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={selectedDocumentContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Document editor"}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={_onEditorContentChange}
|
||||
enableWordWrapContextMenuItem={true}
|
||||
/>
|
||||
)}
|
||||
{selectedRows.size > 1 && (
|
||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
||||
)}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
</div>
|
||||
{bulkDeleteOperation && (
|
||||
<ProgressModalDialog
|
||||
|
||||
@@ -67,6 +67,13 @@ jest.mock("Explorer/Controls/Dialog", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Added as recent change to @azure/core-util would cause randomUUID() to throw an error during jest tests.
|
||||
// TODO: when not using beta version of @azure/cosmos sdk try removing this
|
||||
jest.mock("@azure/core-util", () => ({
|
||||
...jest.requireActual("@azure/core-util"),
|
||||
randomUUID: jest.fn(),
|
||||
}));
|
||||
|
||||
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||
let newWrapper;
|
||||
await act(async () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ describe("DocumentsTableComponent", () => {
|
||||
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
|
||||
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
|
||||
],
|
||||
onItemClicked: (): void => {},
|
||||
onSelectedRowsChange: (): void => {},
|
||||
selectedRows: new Set<TableRowId>(),
|
||||
size: {
|
||||
|
||||
@@ -38,14 +38,13 @@ import {
|
||||
TextSortDescendingRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { NormalizedEventKey } from "Common/Constants";
|
||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
||||
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
||||
import {
|
||||
ColumnSizesMap,
|
||||
ColumnSort,
|
||||
deleteSubComponentState,
|
||||
readSubComponentState,
|
||||
saveSubComponentState,
|
||||
deleteDocumentsTabSubComponentState,
|
||||
readDocumentsTabSubComponentState,
|
||||
saveDocumentsTabSubComponentState,
|
||||
SubComponentName,
|
||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||
@@ -69,7 +68,6 @@ export type ColumnDefinition = {
|
||||
export interface IDocumentsTableComponentProps {
|
||||
onRefreshTable: () => void;
|
||||
items: DocumentsTableComponentItem[];
|
||||
onItemClicked: (index: number) => void;
|
||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||
selectedRows: Set<TableRowId>;
|
||||
size: { height: number; width: number };
|
||||
@@ -99,6 +97,7 @@ const defaultSize = {
|
||||
idealWidth: 200,
|
||||
minWidth: 50,
|
||||
};
|
||||
|
||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||
onRefreshTable,
|
||||
items,
|
||||
@@ -116,8 +115,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
}: IDocumentsTableComponentProps) => {
|
||||
const styles = useDocumentsTabStyles();
|
||||
|
||||
const sortedRowsRef = React.useRef(null);
|
||||
|
||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
|
||||
SubComponentName.ColumnSizes,
|
||||
collection,
|
||||
{},
|
||||
);
|
||||
const columnSizesPx: TableColumnSizingOptions = {};
|
||||
selectedColumnIds.forEach((columnId) => {
|
||||
if (
|
||||
@@ -141,7 +146,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
sortDirection: "ascending" | "descending";
|
||||
sortColumn: TableColumnId | undefined;
|
||||
}>(() => {
|
||||
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||
|
||||
if (!sort) {
|
||||
return {
|
||||
@@ -173,7 +178,12 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
return acc;
|
||||
}, {} as ColumnSizesMap);
|
||||
|
||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
||||
saveDocumentsTabSubComponentState<ColumnSizesMap>(
|
||||
SubComponentName.ColumnSizes,
|
||||
collection,
|
||||
persistentSizes,
|
||||
true,
|
||||
);
|
||||
|
||||
return newSizingOptions;
|
||||
});
|
||||
@@ -185,11 +195,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
setColumnSort(event, columnId, direction);
|
||||
|
||||
if (columnId === undefined || direction === undefined) {
|
||||
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
||||
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
|
||||
return;
|
||||
}
|
||||
|
||||
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
||||
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, {
|
||||
columnId,
|
||||
direction,
|
||||
});
|
||||
};
|
||||
|
||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||
@@ -228,31 +241,29 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||
Refresh
|
||||
</MenuItem>
|
||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) && (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<TextSortAscendingRegular />}
|
||||
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||
>
|
||||
Sort ascending
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<TextSortAscendingRegular />}
|
||||
onClick={(e) => onSortClick(e, column.id, "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
|
||||
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>
|
||||
)}
|
||||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
<MenuDivider />
|
||||
</>
|
||||
<MenuItem
|
||||
key="keyboardresize"
|
||||
icon={<TableResizeColumnRegular />}
|
||||
@@ -260,25 +271,24 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
>
|
||||
Resize with left/right arrow keys
|
||||
</MenuItem>
|
||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) &&
|
||||
!isColumnSelectionDisabled && (
|
||||
<MenuItem
|
||||
key="remove"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => {
|
||||
// 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>
|
||||
)}
|
||||
{!isColumnSelectionDisabled && (
|
||||
<MenuItem
|
||||
key="remove"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => {
|
||||
// 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>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
@@ -295,22 +305,42 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
|
||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
||||
const onTableCellClicked = useCallback(
|
||||
(e: React.MouseEvent, index: number) => {
|
||||
(e: React.MouseEvent | undefined, index: number, rowId: TableRowId) => {
|
||||
if (isSelectionDisabled) {
|
||||
// Only allow click
|
||||
onSelectedRowsChange(new Set<TableRowId>([index]));
|
||||
onSelectedRowsChange(new Set<TableRowId>([rowId]));
|
||||
setSelectionStartIndex(index);
|
||||
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(
|
||||
selectedRows as Set<number>,
|
||||
selectedRowsIndex,
|
||||
index,
|
||||
isEnvironmentShiftPressed(e),
|
||||
isEnvironmentCtrlPressed(e),
|
||||
e && isEnvironmentShiftPressed(e),
|
||||
e && isEnvironmentCtrlPressed(e),
|
||||
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) {
|
||||
setSelectionStartIndex(result.selectionStartIndex);
|
||||
}
|
||||
@@ -324,16 +354,20 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
* - a key is down and the cell is clicked by the mouse
|
||||
*/
|
||||
const onIdClicked = useCallback(
|
||||
(e: React.KeyboardEvent<Element>, index: number) => {
|
||||
(e: React.KeyboardEvent<Element>, rowId: TableRowId) => {
|
||||
if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) {
|
||||
onSelectedRowsChange(new Set<TableRowId>([index]));
|
||||
onSelectedRowsChange(new Set<TableRowId>([rowId]));
|
||||
}
|
||||
},
|
||||
[onSelectedRowsChange],
|
||||
);
|
||||
|
||||
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 (
|
||||
<TableRow
|
||||
aria-rowindex={index + 2}
|
||||
@@ -363,8 +397,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
key={column.columnId}
|
||||
className={styles.tableCell}
|
||||
// When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick.
|
||||
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index)}
|
||||
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, index)}
|
||||
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index, rowId)}
|
||||
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, rowId)}
|
||||
{...columnSizing.getTableCellProps(column.columnId)}
|
||||
tabIndex={column.columnId === "id" ? 0 : -1}
|
||||
>
|
||||
@@ -424,6 +458,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(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === " ") {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<span>
|
||||
SELECT * FROM c
|
||||
SELECT * FROM c
|
||||
</span>
|
||||
<span
|
||||
className="___r7kt3y0_0000000 fqerorx"
|
||||
<InputDataList
|
||||
bottomLink={
|
||||
{
|
||||
"text": "Learn more",
|
||||
"url": "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer",
|
||||
}
|
||||
}
|
||||
dropdownOptions={
|
||||
[
|
||||
{
|
||||
"label": "Default filters",
|
||||
"options": [
|
||||
"WHERE c.id = "foo"",
|
||||
"ORDER BY c._ts DESC",
|
||||
"WHERE c.id = "foo" ORDER BY c._ts DESC",
|
||||
"ORDER BY c._ts ASC",
|
||||
"WHERE c.foo = "foo"",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||
title="Type a query predicate or choose one from the list."
|
||||
value=""
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
aria-label="Apply filter"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
size="small"
|
||||
tabIndex={0}
|
||||
>
|
||||
Edit Filter
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"height": "100%",
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
<Allotment
|
||||
onDragEnd={[Function]}
|
||||
>
|
||||
<Allotment
|
||||
onDragEnd={[Function]}
|
||||
<Allotment.Pane
|
||||
minSize={55}
|
||||
preferredSize="35%"
|
||||
>
|
||||
<Allotment.Pane
|
||||
minSize={55}
|
||||
preferredSize="35%"
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"height": "100%",
|
||||
"overflow": "hidden",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"height": "100%",
|
||||
"overflow": "hidden",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
className="___9o87uj0_0000000 ffefeo0"
|
||||
>
|
||||
<div
|
||||
className="___9o87uj0_0000000 ffefeo0"
|
||||
style={
|
||||
{
|
||||
"height": "100%",
|
||||
"width": "calc(100% + -11px)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
<DocumentsTableComponent
|
||||
collection={
|
||||
{
|
||||
"height": "100%",
|
||||
"width": "calc(100% + -11px)",
|
||||
"databaseId": "databaseId",
|
||||
"id": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<DocumentsTableComponent
|
||||
collection={
|
||||
columnDefinitions={
|
||||
[
|
||||
{
|
||||
"databaseId": "databaseId",
|
||||
"id": [Function],
|
||||
}
|
||||
}
|
||||
columnDefinitions={
|
||||
[
|
||||
{
|
||||
"id": "id",
|
||||
"isPartitionKey": false,
|
||||
"label": "id",
|
||||
},
|
||||
]
|
||||
}
|
||||
defaultColumnSelection={
|
||||
[
|
||||
"id",
|
||||
]
|
||||
}
|
||||
isColumnSelectionDisabled={false}
|
||||
isRowSelectionDisabled={true}
|
||||
items={[]}
|
||||
onColumnSelectionChange={[Function]}
|
||||
onItemClicked={[Function]}
|
||||
onRefreshTable={[Function]}
|
||||
onSelectedRowsChange={[Function]}
|
||||
selectedColumnIds={
|
||||
[
|
||||
"id",
|
||||
]
|
||||
}
|
||||
selectedRows={
|
||||
Set {
|
||||
0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
"id": "id",
|
||||
"isPartitionKey": false,
|
||||
"label": "id",
|
||||
},
|
||||
]
|
||||
}
|
||||
defaultColumnSelection={
|
||||
[
|
||||
"id",
|
||||
]
|
||||
}
|
||||
isColumnSelectionDisabled={false}
|
||||
isRowSelectionDisabled={true}
|
||||
items={[]}
|
||||
onColumnSelectionChange={[Function]}
|
||||
onRefreshTable={[Function]}
|
||||
onSelectedRowsChange={[Function]}
|
||||
selectedColumnIds={
|
||||
[
|
||||
"id",
|
||||
]
|
||||
}
|
||||
selectedRows={Set {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
<Allotment.Pane
|
||||
minSize={30}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
<Allotment.Pane
|
||||
minSize={30}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
/>
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
</div>
|
||||
</CosmosFluentProvider>
|
||||
`;
|
||||
|
||||
@@ -39,7 +39,6 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
||||
},
|
||||
]
|
||||
}
|
||||
onItemClicked={[Function]}
|
||||
onRefreshTable={[Function]}
|
||||
onSelectedRowsChange={[Function]}
|
||||
selectedColumnIds={[]}
|
||||
@@ -504,7 +503,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
||||
},
|
||||
]
|
||||
}
|
||||
onItemClicked={[Function]}
|
||||
onRefreshTable={[Function]}
|
||||
onSelectedRowsChange={[Function]}
|
||||
selectedColumnIds={[]}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ActionType, TabKind } from "Contracts/ActionContracts";
|
||||
import React from "react";
|
||||
import MongoUtility from "../../../Common/MongoUtility";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
@@ -20,7 +21,7 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
private mongoQueryTabProps: IMongoQueryTabProps,
|
||||
) {
|
||||
super(options, mongoQueryTabProps);
|
||||
this.queryText = "";
|
||||
this.queryText = options.queryText ?? "";
|
||||
this.iMongoQueryTabComponentProps = {
|
||||
collection: options.collection,
|
||||
isExecutionError: this.isExecutionError(),
|
||||
@@ -28,6 +29,8 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
tabsBaseInstance: this,
|
||||
queryText: this.queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
splitterDirection: options.splitterDirection,
|
||||
queryViewSizePercent: options.queryViewSizePercent,
|
||||
container: this.mongoQueryTabProps.container,
|
||||
onTabAccessor: (instance: ITabAccessor): void => {
|
||||
this.iTabAccessor = instance;
|
||||
@@ -35,6 +38,26 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
isPreferredApiMongoDB: true,
|
||||
monacoEditorSetting: "plaintext",
|
||||
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
||||
onUpdatePersistedState: (state: {
|
||||
queryText: string;
|
||||
splitterDirection: string;
|
||||
queryViewSizePercent: number;
|
||||
}): void => {
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: TabKind.SQLQuery,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
query: {
|
||||
text: state.queryText,
|
||||
},
|
||||
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
||||
queryViewSizePercent: state.queryViewSizePercent,
|
||||
};
|
||||
if (this.triggerPersistState) {
|
||||
this.triggerPersistState();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||
|
||||
this.state = {
|
||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
|
||||
import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -26,6 +27,8 @@ export class NewQueryTab extends TabsBase {
|
||||
public iQueryTabComponentProps: IQueryTabComponentProps;
|
||||
public iTabAccessor: ITabAccessor;
|
||||
|
||||
protected persistedState: OpenQueryTab;
|
||||
|
||||
constructor(
|
||||
options: QueryTabOptions,
|
||||
private props: IQueryTabProps,
|
||||
@@ -39,12 +42,41 @@ export class NewQueryTab extends TabsBase {
|
||||
tabsBaseInstance: this,
|
||||
queryText: options.queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
splitterDirection: options.splitterDirection,
|
||||
queryViewSizePercent: options.queryViewSizePercent,
|
||||
container: this.props.container,
|
||||
onTabAccessor: (instance: ITabAccessor): void => {
|
||||
this.iTabAccessor = instance;
|
||||
},
|
||||
isPreferredApiMongoDB: false,
|
||||
onUpdatePersistedState: (state: {
|
||||
queryText: string;
|
||||
splitterDirection: string;
|
||||
queryViewSizePercent: number;
|
||||
}): void => {
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: TabKind.SQLQuery,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
query: {
|
||||
text: state.queryText,
|
||||
},
|
||||
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
||||
queryViewSizePercent: state.queryViewSizePercent,
|
||||
};
|
||||
if (this.triggerPersistState) {
|
||||
this.triggerPersistState();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// set initial state
|
||||
this.iQueryTabComponentProps.onUpdatePersistedState({
|
||||
queryText: options.queryText,
|
||||
splitterDirection: options.splitterDirection,
|
||||
queryViewSizePercent: options.queryViewSizePercent,
|
||||
});
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
||||
@@ -34,6 +34,7 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||
AppStateComponentNames: {
|
||||
QueryCopilot: "QueryCopilot",
|
||||
},
|
||||
readSubComponentState: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("QueryTabComponent", () => {
|
||||
|
||||
@@ -18,13 +18,7 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import {
|
||||
LocalStorageUtility,
|
||||
StorageKey,
|
||||
getDefaultQueryResultsView,
|
||||
getRUThreshold,
|
||||
ruThresholdEnabled,
|
||||
} from "Shared/StorageUtility";
|
||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { Allotment } from "allotment";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -99,6 +93,13 @@ export interface IQueryTabComponentProps {
|
||||
copilotEnabled?: boolean;
|
||||
isSampleCopilotActive?: boolean;
|
||||
copilotStore?: Partial<QueryCopilotState>;
|
||||
splitterDirection?: "horizontal" | "vertical";
|
||||
queryViewSizePercent?: number;
|
||||
onUpdatePersistedState: (state: {
|
||||
queryText: string;
|
||||
splitterDirection: "vertical" | "horizontal";
|
||||
queryViewSizePercent: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface IQueryTabStates {
|
||||
@@ -118,11 +119,13 @@ interface IQueryTabStates {
|
||||
queryResultsView: SplitterDirection;
|
||||
errors?: QueryError[];
|
||||
modelMarkers?: monaco.editor.IMarkerData[];
|
||||
queryViewSizePercent: number;
|
||||
}
|
||||
|
||||
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
||||
const styles = useQueryTabStyles();
|
||||
const copilotStore = useCopilotStore();
|
||||
|
||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||
const queryTabProps = {
|
||||
...props,
|
||||
@@ -132,12 +135,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
|
||||
isSampleCopilotActive: isSampleCopilotActive,
|
||||
copilotStore: copilotStore,
|
||||
};
|
||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
|
||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />;
|
||||
};
|
||||
|
||||
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
||||
const styles = useQueryTabStyles();
|
||||
return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
|
||||
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />;
|
||||
};
|
||||
|
||||
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
||||
@@ -146,6 +149,8 @@ type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
||||
|
||||
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
|
||||
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
||||
private static readonly DEBOUNCE_DELAY_MS = 1000;
|
||||
|
||||
public queryEditorId: string;
|
||||
public executeQueryButton: Button;
|
||||
public saveQueryButton: Button;
|
||||
@@ -157,10 +162,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
private _iterator: MinimalQueryIterator;
|
||||
private queryAbortController: AbortController;
|
||||
queryEditor: React.RefObject<EditorReact>;
|
||||
private timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor(props: QueryTabComponentImplProps) {
|
||||
super(props);
|
||||
|
||||
this.queryEditor = createRef<EditorReact>();
|
||||
|
||||
this.state = {
|
||||
@@ -176,7 +181,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
cancelQueryTimeoutID: undefined,
|
||||
copilotActive: this._queryCopilotActive(),
|
||||
currentTabActive: true,
|
||||
queryResultsView: getDefaultQueryResultsView(),
|
||||
queryResultsView:
|
||||
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
|
||||
queryViewSizePercent: props.queryViewSizePercent,
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
@@ -207,6 +214,23 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to save the query text in the query tab state
|
||||
* Since it reads and writes to the same state, it is debounced
|
||||
*/
|
||||
private saveQueryTabStateDebounced = () => {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
this.timeoutId = setTimeout(async () => {
|
||||
this.props.onUpdatePersistedState({
|
||||
queryText: this.state.sqlQueryEditorContent,
|
||||
splitterDirection: this.state.queryResultsView,
|
||||
queryViewSizePercent: this.state.queryViewSizePercent,
|
||||
});
|
||||
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
|
||||
};
|
||||
|
||||
private _queryCopilotActive(): boolean {
|
||||
if (this.props.copilotEnabled) {
|
||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||
@@ -567,7 +591,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
};
|
||||
}
|
||||
private _setViewLayout(direction: SplitterDirection): void {
|
||||
this.setState({ queryResultsView: direction });
|
||||
this.setState({ queryResultsView: direction }, () => this.saveQueryTabStateDebounced());
|
||||
|
||||
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||
setTimeout(() => {
|
||||
@@ -599,13 +623,16 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
this.setState(
|
||||
{
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
|
||||
// Clear the markers when the user edits the document.
|
||||
modelMarkers: [],
|
||||
});
|
||||
// Clear the markers when the user edits the document.
|
||||
modelMarkers: [],
|
||||
},
|
||||
() => this.saveQueryTabStateDebounced(),
|
||||
);
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
if (newContent.length > 0) {
|
||||
this.executeQueryButton = {
|
||||
@@ -704,8 +731,20 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
></QueryCopilotPromptbar>
|
||||
)}
|
||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||
<Allotment key={vertical.toString()} vertical={vertical}>
|
||||
<Allotment.Pane data-test="QueryTab/EditorPane">
|
||||
<Allotment
|
||||
key={vertical.toString()}
|
||||
vertical={vertical}
|
||||
onDragEnd={(sizes: number[]) => {
|
||||
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||
this.setState({ queryViewSizePercent }, () => this.saveQueryTabStateDebounced());
|
||||
}}
|
||||
>
|
||||
<Allotment.Pane
|
||||
data-test="QueryTab/EditorPane"
|
||||
preferredSize={
|
||||
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
|
||||
}
|
||||
>
|
||||
<EditorReact
|
||||
ref={this.queryEditor}
|
||||
className={this.props.styles.queryEditor}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||
import React from "react";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||
@@ -10,6 +11,18 @@ export class SettingsTabV2 extends TabsBase {
|
||||
}
|
||||
|
||||
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
||||
protected persistedState: OpenCollectionTab;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: TabKind.ScaleSettings,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
};
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
super.onActivate();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react";
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { IpRule } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -35,7 +33,7 @@ interface TabsProps {
|
||||
}
|
||||
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||
const [
|
||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
@@ -60,29 +58,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{networkSettingsWarning && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
styles={defaultMessageBarStyles}
|
||||
actions={
|
||||
<MessageBarButton
|
||||
onClick={() =>
|
||||
sendMessage({
|
||||
type:
|
||||
userContext.apiType === "VCoreMongo"
|
||||
? MessageTypes.OpenVCoreMongoNetworkingBlade
|
||||
: MessageTypes.OpenPostgresNetworkingBlade,
|
||||
})
|
||||
}
|
||||
>
|
||||
Change network settings
|
||||
</MessageBarButton>
|
||||
}
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
>
|
||||
{networkSettingsWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
@@ -228,7 +203,7 @@ const CloseButton = ({
|
||||
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
|
||||
>
|
||||
<span className="tabIcon close-Icon">
|
||||
<img src={errorIcon} title="Close" alt="Close" role="none" />
|
||||
<img src={errorIcon} title="Close" alt="Close" aria-label="hidden" />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
@@ -343,7 +318,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
ipRules?.length
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OpenTab } from "Contracts/ActionContracts";
|
||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -30,6 +31,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
protected _theme: string;
|
||||
public onLoadStartKey: number;
|
||||
|
||||
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super();
|
||||
this.index = options.index;
|
||||
@@ -55,6 +58,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
};
|
||||
}
|
||||
|
||||
// Called by useTabs to persist
|
||||
public getPersistedState = (): OpenTab | null => this.persistedState;
|
||||
public triggerPersistState: () => void = undefined;
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
|
||||
@@ -52,6 +52,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
public partitionKeyProperties: string[];
|
||||
public id: ko.Observable<string>;
|
||||
public defaultTtl: ko.Observable<number>;
|
||||
public vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
||||
public fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
public usageSizeInKB: ko.Observable<number>;
|
||||
@@ -110,6 +112,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
this.id = ko.observable(data.id);
|
||||
this.defaultTtl = ko.observable(data.defaultTtl);
|
||||
this.vectorEmbeddingPolicy = ko.observable(data.vectorEmbeddingPolicy);
|
||||
this.fullTextPolicy = ko.observable(data.fullTextPolicy);
|
||||
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
||||
this.usageSizeInKB = ko.observable();
|
||||
this.offer = ko.observable();
|
||||
@@ -626,7 +630,13 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
};
|
||||
|
||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||
public onNewQueryClick(
|
||||
source: any,
|
||||
event: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
) {
|
||||
const collection: ViewModels.Collection = source.collection || source;
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
const title = "Query " + id;
|
||||
@@ -649,13 +659,21 @@ export default class Collection implements ViewModels.Collection {
|
||||
queryText: queryText,
|
||||
partitionKey: collection.partitionKey,
|
||||
onLoadStartKey: startKey,
|
||||
splitterDirection,
|
||||
queryViewSizePercent,
|
||||
},
|
||||
{ container: this.container },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||
public onNewMongoQueryClick(
|
||||
source: any,
|
||||
event: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
) {
|
||||
const collection: ViewModels.Collection = source.collection || source;
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
|
||||
@@ -677,6 +695,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
node: this,
|
||||
partitionKey: collection.partitionKey,
|
||||
onLoadStartKey: startKey,
|
||||
queryText,
|
||||
splitterDirection,
|
||||
queryViewSizePercent,
|
||||
},
|
||||
{
|
||||
container: this.container,
|
||||
|
||||
@@ -83,8 +83,8 @@ const bindings: Record<KeyboardAction, string[]> = {
|
||||
[KeyboardAction.NEW_ITEM]: ["Alt+N I"],
|
||||
[KeyboardAction.DELETE_ITEM]: ["Alt+D"],
|
||||
[KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"],
|
||||
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
|
||||
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
|
||||
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+BracketLeft", "$mod+Shift+F6"],
|
||||
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+BracketRight", "$mod+F6"],
|
||||
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
|
||||
[KeyboardAction.SEARCH]: ["$mod+Shift+F"],
|
||||
[KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"],
|
||||
|
||||
@@ -266,7 +266,10 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
||||
method: "GET",
|
||||
apiVersion: "2023-05-01-preview",
|
||||
queryParams: {
|
||||
filter: "armRegionName eq '" + regionShortName + "'",
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
regionShortName +
|
||||
"' and productDisplayName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { userContext } from "UserContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
// The component name whose state is being saved. Component name must not include special characters.
|
||||
export enum AppStateComponentNames {
|
||||
DocumentsTab = "DocumentsTab",
|
||||
MostRecentActivity = "MostRecentActivity",
|
||||
QueryCopilot = "QueryCopilot",
|
||||
DataExplorerAction = "DataExplorerAction",
|
||||
}
|
||||
|
||||
// Subcomponent for DataExplorerAction
|
||||
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
|
||||
|
||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
@@ -72,12 +80,18 @@ export const hasState = (path: StorePath): boolean => {
|
||||
};
|
||||
|
||||
// This is for high-frequency state changes
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
// Keep track of timeouts per path
|
||||
const pathToTimeoutIdMap = new Map<string, NodeJS.Timeout>();
|
||||
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
||||
const key = createKeyFromPath(path);
|
||||
const timeoutId = pathToTimeoutIdMap.get(key);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
|
||||
pathToTimeoutIdMap.set(
|
||||
key,
|
||||
setTimeout(() => saveState(path, state), debounceDelayMs),
|
||||
);
|
||||
};
|
||||
|
||||
interface ApplicationState {
|
||||
@@ -112,3 +126,93 @@ export const createKeyFromPath = (path: StorePath): string => {
|
||||
export const deleteAllStates = (): void => {
|
||||
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
||||
};
|
||||
|
||||
// Convenience functions
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param defaultValue Will be returned if persisted state is not found
|
||||
* @returns
|
||||
*/
|
||||
export const readSubComponentState = <T>(
|
||||
componentName: AppStateComponentNames,
|
||||
subComponentName: string,
|
||||
collection: ViewModels.CollectionBase | undefined,
|
||||
defaultValue: T,
|
||||
): T => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const state = loadState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection ? collection.databaseId : "",
|
||||
containerName: collection ? collection.id() : "",
|
||||
}) as T;
|
||||
|
||||
return state || defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param state State to save
|
||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
||||
*/
|
||||
export const saveSubComponentState = <T>(
|
||||
componentName: AppStateComponentNames,
|
||||
subComponentName: string,
|
||||
collection: ViewModels.CollectionBase | undefined,
|
||||
state: T,
|
||||
debounce?: boolean,
|
||||
): void => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
(debounce ? saveStateDebounced : saveState)(
|
||||
{
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection ? collection.databaseId : "",
|
||||
containerName: collection ? collection.id() : "",
|
||||
},
|
||||
state,
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteSubComponentState = (
|
||||
componentName: AppStateComponentNames,
|
||||
subComponentName: string,
|
||||
collection: ViewModels.CollectionBase,
|
||||
) => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
deleteState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -74,6 +74,8 @@ export interface UserContext {
|
||||
readonly authType?: AuthType;
|
||||
readonly masterKey?: string;
|
||||
readonly subscriptionId?: string;
|
||||
readonly tenantId?: string;
|
||||
readonly userName?: string;
|
||||
readonly resourceGroup?: string;
|
||||
readonly databaseAccount?: DatabaseAccount;
|
||||
readonly endpoint?: string;
|
||||
@@ -102,7 +104,7 @@ export interface UserContext {
|
||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||
readonly dataPlaneRbacEnabled?: boolean;
|
||||
readonly hasDataPlaneRbacSettingChanged?: boolean;
|
||||
readonly refreshCosmosClient?: boolean;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as msal from "@azure/msal-browser";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
@@ -64,7 +65,85 @@ export async function getMsalInstance() {
|
||||
return msalInstance;
|
||||
}
|
||||
|
||||
export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) {
|
||||
export async function acquireMsalTokenForAccount(
|
||||
account: DatabaseAccount,
|
||||
silent: boolean = false,
|
||||
user_hint?: string,
|
||||
) {
|
||||
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
||||
throw new Error("Database account has no document endpoint defined");
|
||||
}
|
||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||
/\/+$/,
|
||||
"/.default",
|
||||
);
|
||||
const msalInstance = await getMsalInstance();
|
||||
const knownAccounts = msalInstance.getAllAccounts();
|
||||
// If user_hint is provided, we will try to use it to find the account.
|
||||
// If no account is found, we will use the current active account or first account in the list.
|
||||
const msalAccount =
|
||||
knownAccounts?.filter((account) => account.username === user_hint)[0] ??
|
||||
msalInstance.getActiveAccount() ??
|
||||
knownAccounts?.[0];
|
||||
|
||||
if (!msalAccount) {
|
||||
// If no account was found, we need to sign in.
|
||||
// This will eventually throw InteractionRequiredAuthError if silent is true, we won't handle it here.
|
||||
const loginRequest = {
|
||||
scopes: [hrefEndpoint],
|
||||
loginHint: user_hint ?? userContext.userName,
|
||||
authority: userContext.tenantId ? `${configContext.AAD_ENDPOINT}${userContext.tenantId}` : undefined,
|
||||
};
|
||||
try {
|
||||
if (silent) {
|
||||
// We can try to use SSO between different apps to avoid showing a popup.
|
||||
// With a hint provided, this should work in most cases.
|
||||
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-sso#sso-between-different-apps
|
||||
try {
|
||||
const loginResponse = await msalInstance.ssoSilent(loginRequest);
|
||||
return loginResponse.accessToken;
|
||||
} catch (silentError) {
|
||||
trace(Action.SignInAad, ActionModifiers.Mark, {
|
||||
request: JSON.stringify(loginRequest),
|
||||
acquireTokenType: silent ? "silent" : "interactive",
|
||||
errorMessage: JSON.stringify(silentError),
|
||||
});
|
||||
}
|
||||
}
|
||||
// If silent acquisition failed, we need to show a popup.
|
||||
// Passing prompt: "none" will still show a popup but not perform a full sign-in.
|
||||
// This will only work if the user has already signed in and the session is still valid.
|
||||
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-prompt-behavior#interactive-requests-with-promptnone
|
||||
// The hint will be used to pre-fill the username field in the popup if silent is false.
|
||||
const loginResponse = await msalInstance.loginPopup({ prompt: silent ? "none" : "login", ...loginRequest });
|
||||
return loginResponse.accessToken;
|
||||
} catch (error) {
|
||||
traceFailure(Action.SignInAad, {
|
||||
request: JSON.stringify(loginRequest),
|
||||
acquireTokenType: silent ? "silent" : "interactive",
|
||||
errorMessage: JSON.stringify(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
msalInstance.setActiveAccount(msalAccount);
|
||||
}
|
||||
|
||||
const tokenRequest = {
|
||||
account: msalAccount || null,
|
||||
forceRefresh: true,
|
||||
scopes: [hrefEndpoint],
|
||||
loginHint: user_hint ?? userContext.userName,
|
||||
authority: `${configContext.AAD_ENDPOINT}${userContext.tenantId ?? msalAccount.tenantId}`,
|
||||
};
|
||||
return acquireTokenWithMsal(msalInstance, tokenRequest, silent);
|
||||
}
|
||||
|
||||
export async function acquireTokenWithMsal(
|
||||
msalInstance: msal.IPublicClientApplication,
|
||||
request: msal.SilentRequest,
|
||||
silent: boolean = false,
|
||||
) {
|
||||
const tokenRequest = {
|
||||
account: msalInstance.getActiveAccount() || null,
|
||||
...request,
|
||||
@@ -74,7 +153,7 @@ export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientAppli
|
||||
// attempt silent acquisition first
|
||||
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
|
||||
} catch (silentError) {
|
||||
if (silentError instanceof msal.InteractionRequiredAuthError) {
|
||||
if (silentError instanceof msal.InteractionRequiredAuthError && silent === false) {
|
||||
try {
|
||||
// The error indicates that we need to acquire the token interactively.
|
||||
// This will display a pop-up to re-establish authorization. If user does not
|
||||
|
||||
@@ -20,3 +20,7 @@ export const isServerlessAccount = (): boolean => {
|
||||
export const isVectorSearchEnabled = (): boolean => {
|
||||
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
|
||||
};
|
||||
|
||||
export const isFullTextSearchEnabled = (): boolean => {
|
||||
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearch);
|
||||
};
|
||||
|
||||
@@ -92,8 +92,8 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
||||
};
|
||||
|
||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Local,
|
||||
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
@@ -108,7 +108,7 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> =
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const allowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
@@ -141,9 +141,7 @@ export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspac
|
||||
|
||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/",
|
||||
];
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = ["https://dataexplorer-preview.azurewebsites.net/"];
|
||||
|
||||
export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
JunoEndpoints.Test,
|
||||
|
||||
@@ -13,9 +13,9 @@ describe("isInvalidParentFrameOrigin", () => {
|
||||
${"https://subdomain.portal.azure.com"} | ${false}
|
||||
${"https://subdomain.portal.azure.us"} | ${false}
|
||||
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||
${"https://cdb-ms-prod-pbe.cosmos.azure.com"} | ${false}
|
||||
${"https://cdb-ff-prod-pbe.cosmos.azure.us"} | ${false}
|
||||
${"https://cdb-mc-prod-pbe.cosmos.azure.cn"} | ${false}
|
||||
${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false}
|
||||
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||
${"https://random.domain"} | ${true}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import { resetConfigContext, updateConfigContext } from "ConfigContext";
|
||||
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
import { MongoProxyOutboundIPs, PortalBackendIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
||||
|
||||
describe("NetworkUtility tests", () => {
|
||||
describe("getNetworkSettingsWarningMessage", () => {
|
||||
const legacyBackendEndpoint: string = "https://main.documentdb.ext.azure.com";
|
||||
const publicAccessMessagePart = "Please enable public access to proceed";
|
||||
const accessMessagePart = "Please allow access from Azure Portal to proceed";
|
||||
let warningMessageResult: string;
|
||||
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
|
||||
|
||||
beforeEach(() => {
|
||||
warningMessageResult = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfigContext();
|
||||
});
|
||||
|
||||
it("should return no message when publicNetworkAccess is enabled", async () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
await getNetworkSettingsWarningMessage(warningMessageFunc);
|
||||
expect(warningMessageResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return publicAccessMessage when publicNetworkAccess is disabled", async () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
publicNetworkAccess: "Disabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
await getNetworkSettingsWarningMessage(warningMessageFunc);
|
||||
expect(warningMessageResult).toContain(publicAccessMessagePart);
|
||||
});
|
||||
|
||||
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => {
|
||||
const portalBackendOutboundIPsWithLegacyIPs: string[] = [
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||
...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac],
|
||||
...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod],
|
||||
...PortalBackendIPs["https://main.documentdb.ext.azure.com"],
|
||||
];
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
kind: "MongoDB",
|
||||
properties: {
|
||||
ipRules: portalBackendOutboundIPsWithLegacyIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: legacyBackendEndpoint,
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
});
|
||||
|
||||
let asyncWarningMessageResult: string;
|
||||
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
|
||||
|
||||
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
|
||||
expect(asyncWarningMessageResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return accessMessage when incorrent ip rule is added to mongo/cassandra account per endpoint", async () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
kind: "MongoDB",
|
||||
properties: {
|
||||
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: legacyBackendEndpoint,
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
});
|
||||
|
||||
let asyncWarningMessageResult: string;
|
||||
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
|
||||
|
||||
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
|
||||
expect(asyncWarningMessageResult).toContain(accessMessagePart);
|
||||
});
|
||||
|
||||
// Postgres and vcore mongo account checks basically pass through to CheckFirewallRules so those
|
||||
// tests are omitted here and included in CheckFirewallRules.test.ts
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import { userContext } from "UserContext";
|
||||
import {
|
||||
CassandraProxyOutboundIPs,
|
||||
MongoProxyOutboundIPs,
|
||||
PortalBackendIPs,
|
||||
PortalBackendOutboundIPs,
|
||||
} from "Utils/EndpointUtils";
|
||||
|
||||
export const getNetworkSettingsWarningMessage = async (
|
||||
setStateFunc: (warningMessage: string) => void,
|
||||
): Promise<void> => {
|
||||
const accountProperties = userContext.databaseAccount?.properties;
|
||||
const accessMessage =
|
||||
"The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
|
||||
const publicAccessMessage =
|
||||
"The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
||||
|
||||
if (userContext.apiType === "Postgres") {
|
||||
checkFirewallRules(
|
||||
"2022-11-08",
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||
undefined,
|
||||
setStateFunc,
|
||||
accessMessage,
|
||||
);
|
||||
return;
|
||||
} else if (userContext.apiType === "VCoreMongo") {
|
||||
checkFirewallRules(
|
||||
"2023-03-01-preview",
|
||||
(rule) =>
|
||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
||||
undefined,
|
||||
setStateFunc,
|
||||
accessMessage,
|
||||
);
|
||||
return;
|
||||
} else if (accountProperties) {
|
||||
// public network access is disabled
|
||||
if (
|
||||
accountProperties.publicNetworkAccess !== "Enabled" &&
|
||||
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
|
||||
) {
|
||||
setStateFunc(publicAccessMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const ipRules = accountProperties.ipRules;
|
||||
// public network access is NOT set to "All networks"
|
||||
if (ipRules?.length > 0) {
|
||||
const isProdOrMpacPortalBackendEndpoint: boolean = [
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
const portalBackendOutboundIPs: string[] = isProdOrMpacPortalBackendEndpoint
|
||||
? [
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||
]
|
||||
: PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||
let portalIPs: string[] = [...portalBackendOutboundIPs, ...PortalBackendIPs[configContext.BACKEND_ENDPOINT]];
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||
configContext.MONGO_PROXY_ENDPOINT,
|
||||
);
|
||||
|
||||
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||
|
||||
portalIPs = [...portalIPs, ...mongoProxyOutboundIPs];
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
||||
|
||||
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
||||
? [
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
||||
]
|
||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||
|
||||
portalIPs = [...portalIPs, ...cassandraProxyOutboundIPs];
|
||||
}
|
||||
|
||||
let numberOfMatches = 0;
|
||||
ipRules.forEach((ipRule) => {
|
||||
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
|
||||
numberOfMatches++;
|
||||
}
|
||||
});
|
||||
|
||||
if (numberOfMatches !== portalIPs.length) {
|
||||
setStateFunc(accessMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1237,6 +1237,7 @@ export interface SqlContainerResource {
|
||||
id: string;
|
||||
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
|
||||
/* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
@@ -1281,6 +1282,28 @@ export interface VectorEmbedding {
|
||||
distanceFunction?: string;
|
||||
}
|
||||
|
||||
export interface FullTextPolicy {
|
||||
/**
|
||||
* The default language for the full text .
|
||||
*/
|
||||
defaultLanguage: string;
|
||||
/**
|
||||
* The paths to be indexed for full text search.
|
||||
*/
|
||||
fullTextPaths: FullTextPath[];
|
||||
}
|
||||
|
||||
export interface FullTextPath {
|
||||
/**
|
||||
* The path to be indexed for full text search.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* The language for the full text path.
|
||||
*/
|
||||
language: string;
|
||||
}
|
||||
|
||||
/* Cosmos DB indexing policy */
|
||||
export interface IndexingPolicy {
|
||||
/* Indicates if the indexing policy is automatic */
|
||||
@@ -1301,6 +1324,8 @@ export interface IndexingPolicy {
|
||||
spatialIndexes?: SpatialSpec[];
|
||||
|
||||
vectorIndexes?: VectorIndex[];
|
||||
|
||||
fullTextIndexes?: FullTextIndex[];
|
||||
}
|
||||
|
||||
export interface VectorIndex {
|
||||
@@ -1308,6 +1333,11 @@ export interface VectorIndex {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface FullTextIndex {
|
||||
/** The path in the JSON document to index. */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/* undocumented */
|
||||
export interface ExcludedPath {
|
||||
/* The path for which the indexing behavior applies to. Index paths typically start with root and end with wildcard (/path/*) */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user