mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-26 04:11:24 +00:00
Compare commits
3 Commits
before_opt
...
ashleyst/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf8359c548 | ||
|
|
b03925abab | ||
|
|
53d3413c62 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
|||||||
[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
[Preview this branch](https://cosmos-explorer-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 ci
|
||||||
- run: npm run build:contracts
|
- run: npm run build:contracts
|
||||||
- name: Restore Build Cache
|
- name: Restore Build Cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: .cache
|
path: .cache
|
||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
@@ -92,20 +92,18 @@ jobs:
|
|||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: cp -r ./Contracts ./dist/contracts
|
||||||
- run: cp -r ./configs ./dist/configs
|
- run: cp -r ./configs ./dist/configs
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: 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
|
- name: Upload build to preview blob storage
|
||||||
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
|
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 }}
|
||||||
- name: Upload preview config to blob storage
|
- name: Upload preview config to blob storage
|
||||||
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
|
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 }}
|
||||||
nuget:
|
nuget:
|
||||||
name: Publish Nuget
|
name: Publish Nuget
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
@@ -115,21 +113,21 @@ jobs:
|
|||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: nuget/setup-nuget@v1
|
||||||
|
with:
|
||||||
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/prod.json config.json
|
- run: cp ./configs/prod.json config.json
|
||||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- run: dotnet nuget remove source "ADO"
|
- uses: actions/upload-artifact@v3
|
||||||
- uses: actions/upload-artifact@v4
|
name: packages
|
||||||
name: Upload package to Artifacts
|
|
||||||
with:
|
with:
|
||||||
name: prod-package
|
path: "*.nupkg"
|
||||||
path: "bin/Release/*.nupkg"
|
|
||||||
|
|
||||||
nugetmpac:
|
nugetmpac:
|
||||||
name: Publish Nuget MPAC
|
name: Publish Nuget MPAC
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
@@ -139,21 +137,22 @@ jobs:
|
|||||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: nuget/setup-nuget@v1
|
||||||
|
with:
|
||||||
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/mpac.json config.json
|
- 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: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- run: dotnet nuget remove source "ADO"
|
- uses: actions/upload-artifact@v3
|
||||||
- uses: actions/upload-artifact@v4
|
name: packages
|
||||||
name: Upload package to Artifacts
|
|
||||||
with:
|
with:
|
||||||
name: mpac-package
|
path: "*.nupkg"
|
||||||
path: "bin/Release/*.nupkg"
|
|
||||||
|
|
||||||
playwright-tests:
|
playwright-tests:
|
||||||
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
|
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
|
||||||
@@ -186,9 +185,9 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
path: blob-report
|
path: blob-report
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
merge-playwright-reports:
|
merge-playwright-reports:
|
||||||
name: "Merge Playwright Reports"
|
name: "Merge Playwright Reports"
|
||||||
@@ -198,26 +197,26 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
pattern: blob-report-*
|
pattern: blob-report-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Merge into HTML Report
|
- name: Merge into HTML Report
|
||||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||||
|
|
||||||
- name: Upload HTML report
|
- name: Upload HTML report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
name: html-report--attempt-${{ github.run_attempt }}
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<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,7 +2,6 @@
|
|||||||
|
|
||||||
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)
|
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
|
## Getting Started
|
||||||
@@ -19,7 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
|
|||||||
### Hosted Development (https://cosmos.azure.com)
|
### Hosted Development (https://cosmos.azure.com)
|
||||||
|
|
||||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://cdb-ms-mpac-pbe.cosmos.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://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||||
|
|
||||||
### Emulator Development
|
### Emulator Development
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
||||||
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://cdb-ms-mpac-pbe.cosmos.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://main.documentdb.ext.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
||||||
<h3>Emulator Development</h3>
|
<h3>Emulator Development</h3>
|
||||||
|
|||||||
@@ -174,11 +174,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"],
|
||||||
"/node_modules/(?!@fluentui/react-icons|(.*)/dist/browser)/",
|
|
||||||
"/node_modules/plotly.js-cartesian-dist-min",
|
|
||||||
"/externals/",
|
|
||||||
],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
// unmockedModulePathPatterns: undefined,
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|||||||
@@ -1830,14 +1830,6 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customAccordion button:focus {
|
|
||||||
.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
.customAccordion {
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datalist-arrow:after:hover {
|
.datalist-arrow:after:hover {
|
||||||
content: "\276F";
|
content: "\276F";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -2626,6 +2618,7 @@ a:link {
|
|||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3125,7 +3118,3 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarContainer {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ a:focus {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.splashLoaderContainer {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divExplorer {
|
#divExplorer {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
@@ -31,24 +27,26 @@ a:focus {
|
|||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
@@ -67,16 +65,17 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
|
||||||
|
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: 2px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
||||||
border-bottom: 2px solid @FabricAccentMedium;
|
border-bottom: 2px solid @FabricAccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
||||||
border-bottom: 0px none transparent;
|
border-bottom: 0px none transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +94,10 @@ a:focus {
|
|||||||
padding-bottom: @SmallSpace;
|
padding-bottom: @SmallSpace;
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
@@ -120,6 +119,7 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -156,21 +156,25 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
& > .treeNodeHeader {
|
&>.treeNodeHeader {
|
||||||
background-color: @FabricAccentExtra;
|
background-color: @FabricAccentExtra;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.filterbtnstyle {
|
.filterbtnstyle {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -196,10 +200,12 @@ a:focus {
|
|||||||
border: solid 1px #d1d1d1;
|
border: solid 1px #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gridRowSelected .tabdocumentsGridElement:hover {
|
.gridRowSelected .tabdocumentsGridElement:hover {
|
||||||
background-color: @FabricAccentLight !important;
|
background-color: @FabricAccentLight !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.refreshcol {
|
.refreshcol {
|
||||||
filter: brightness(0) saturate(100%);
|
filter: brightness(0) saturate(100%);
|
||||||
}
|
}
|
||||||
|
|||||||
761
package-lock.json
generated
761
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,10 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.2.0-beta.1",
|
"@azure/cosmos": "4.0.1-beta.3",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "4.5.0",
|
"@azure/identity": "1.5.2",
|
||||||
|
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"@babel/preset-env": "7.24.7",
|
"@babel/preset-env": "7.24.7",
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.24.7",
|
||||||
"@babel/preset-typescript": "7.24.7",
|
"@babel/preset-typescript": "7.24.7",
|
||||||
"@playwright/test": "1.49.1",
|
"@playwright/test": "1.44.0",
|
||||||
"@testing-library/react": "11.2.3",
|
"@testing-library/react": "11.2.3",
|
||||||
"@types/applicationinsights-js": "1.0.7",
|
"@types/applicationinsights-js": "1.0.7",
|
||||||
"@types/codemirror": "0.0.56",
|
"@types/codemirror": "0.0.56",
|
||||||
@@ -169,10 +170,10 @@
|
|||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-canvas-mock": "2.5.2",
|
"jest-canvas-mock": "2.5.2",
|
||||||
"jest-circus": "29.7.0",
|
"jest-circus": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
|
||||||
"jest-html-loader": "1.0.0",
|
"jest-html-loader": "1.0.0",
|
||||||
"jest-react-hooks-shallow": "1.5.1",
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "3.0.2",
|
"jest-trx-results-processor": "3.0.2",
|
||||||
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "11.1.3",
|
"less-loader": "11.1.3",
|
||||||
"less-vars-loader": "1.1.0",
|
"less-vars-loader": "1.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
group = dataexplorer-preview
|
group = stfaul
|
||||||
sku = P1V2
|
sku = P1v2
|
||||||
appserviceplan = dataexplorer-preview
|
appserviceplan = stfaul_asp_Linux_centralus_0
|
||||||
location = westus2
|
location = centralus
|
||||||
web = dataexplorer-preview
|
web = cosmos-explorer-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:
|
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
|
||||||
|
|
||||||
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
Connection string URLs: https://cosmos-explorer-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
|
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-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.
|
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",
|
"PROXY_PATH": "/proxy",
|
||||||
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
|
"msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,8 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
|||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
const api = createProxyMiddleware("/api", {
|
||||||
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
target: "https://main.documentdb.ext.azure.com",
|
||||||
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,
|
changeOrigin: true,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
bypass: (req, res) => {
|
bypass: (req, res) => {
|
||||||
@@ -22,8 +15,8 @@ const api = createProxyMiddleware({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const proxy = createProxyMiddleware({
|
const proxy = createProxyMiddleware("/proxy", {
|
||||||
target: backendEndpoint,
|
target: "https://main.documentdb.ext.azure.com",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
@@ -34,38 +27,35 @@ const proxy = createProxyMiddleware({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const commit = createProxyMiddleware({
|
const commit = createProxyMiddleware("/commit", {
|
||||||
target: previewStorageWebsiteEndpoint,
|
target: "https://cosmosexplorerpreview.blob.core.windows.net",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
pathRewrite: { "^/commit": "/" },
|
pathRewrite: { "^/commit": "$web/" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use("/api", api);
|
app.use(api);
|
||||||
app.use("/proxy", proxy);
|
app.use(proxy);
|
||||||
app.use("/commit", commit);
|
app.use(commit);
|
||||||
app.get("/pull/:pr(\\d+)", (req, res) => {
|
app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||||
const pr = req.params.pr;
|
const pr = req.params.pr;
|
||||||
if (!/^\d+$/.test(pr)) {
|
|
||||||
return res.status(400).send("Invalid pull request number");
|
|
||||||
}
|
|
||||||
const [, query] = req.originalUrl.split("?");
|
const [, query] = req.originalUrl.split("?");
|
||||||
const search = new URLSearchParams(query);
|
const search = new URLSearchParams(query);
|
||||||
|
|
||||||
fetch(`${githubApiUrl}/pulls/${pr}`)
|
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then(({ head: { ref, sha } }) => {
|
.then(({ head: { ref, sha } }) => {
|
||||||
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
|
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
|
||||||
prUrl.hash = ref;
|
prUrl.hash = ref;
|
||||||
search.set("feature.pr", prUrl.href);
|
search.set("feature.pr", prUrl.href);
|
||||||
|
|
||||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
|
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
|
||||||
explorer.search = search.toString();
|
explorer.search = search.toString();
|
||||||
|
|
||||||
const portal = new URL(azurePortalMpacEndpoint);
|
const portal = new URL("https://ms.portal.azure.com/");
|
||||||
portal.searchParams.set("dataExplorerSource", explorer.href);
|
portal.searchParams.set("dataExplorerSource", explorer.href);
|
||||||
|
|
||||||
return res.redirect(portal.href);
|
return res.redirect(portal.href);
|
||||||
@@ -73,10 +63,12 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
|
|||||||
.catch(() => res.sendStatus(500));
|
.catch(() => res.sendStatus(500));
|
||||||
});
|
});
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
fetch(`${githubApiUrl}/branches/master`)
|
fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then(({ commit: { sha } }) => {
|
.then(({ commit: { sha } }) => {
|
||||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`);
|
const explorer = new URL(
|
||||||
|
"https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html"
|
||||||
|
);
|
||||||
return res.redirect(explorer.href);
|
return res.redirect(explorer.href);
|
||||||
})
|
})
|
||||||
.catch(() => res.sendStatus(500));
|
.catch(() => res.sendStatus(500));
|
||||||
|
|||||||
1358
preview/package-lock.json
generated
1358
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@@ -12,8 +12,7 @@
|
|||||||
"author": "Microsoft Corporation",
|
"author": "Microsoft Corporation",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"http-proxy-middleware": "^3.0.3",
|
"http-proxy-middleware": "^1.1.0",
|
||||||
"node": "^18.20.6",
|
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,6 @@ export class CapabilityNames {
|
|||||||
public static readonly EnableMongo: string = "EnableMongo";
|
public static readonly EnableMongo: string = "EnableMongo";
|
||||||
public static readonly EnableServerless: string = "EnableServerless";
|
public static readonly EnableServerless: string = "EnableServerless";
|
||||||
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
||||||
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CapacityMode {
|
export enum CapacityMode {
|
||||||
@@ -97,12 +96,6 @@ export enum CapacityMode {
|
|||||||
Serverless = "Serverless",
|
Serverless = "Serverless",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WorkloadType {
|
|
||||||
Learning = "Learning",
|
|
||||||
DevelopmentTesting = "Development/Testing",
|
|
||||||
Production = "Production",
|
|
||||||
None = "None",
|
|
||||||
}
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
export class Flights {
|
export class Flights {
|
||||||
public static readonly SettingsV2 = "settingsv2";
|
public static readonly SettingsV2 = "settingsv2";
|
||||||
@@ -125,7 +118,6 @@ export class AfecFeatures {
|
|||||||
|
|
||||||
export class TagNames {
|
export class TagNames {
|
||||||
public static defaultExperience: string = "defaultExperience";
|
public static defaultExperience: string = "defaultExperience";
|
||||||
public static WorkloadType: string = "hidden-workload-type";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoDBAccounts {
|
export class MongoDBAccounts {
|
||||||
@@ -156,25 +148,13 @@ export class PortalBackendEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MongoProxyEndpoints {
|
export class MongoProxyEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7238";
|
public static readonly Local: string = "https://localhost:7238";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoProxyApi {
|
|
||||||
public static readonly ResourceList: string = "ResourceList";
|
|
||||||
public static readonly QueryDocuments: string = "QueryDocuments";
|
|
||||||
public static readonly CreateDocument: string = "CreateDocument";
|
|
||||||
public static readonly ReadDocument: string = "ReadDocument";
|
|
||||||
public static readonly UpdateDocument: string = "UpdateDocument";
|
|
||||||
public static readonly DeleteDocument: string = "DeleteDocument";
|
|
||||||
public static readonly CreateCollectionWithProxy: string = "CreateCollectionWithProxy";
|
|
||||||
public static readonly LegacyMongoShell: string = "LegacyMongoShell";
|
|
||||||
public static readonly BulkDelete: string = "BulkDelete";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CassandraProxyEndpoints {
|
export class CassandraProxyEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7240";
|
public static readonly Development: string = "https://localhost:7240";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||||
@@ -525,11 +505,6 @@ export class PriorityLevel {
|
|||||||
public static readonly Default = "low";
|
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 QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PortalBackendEndpoints } from "Common/Constants";
|
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
||||||
|
|
||||||
@@ -21,22 +20,22 @@ describe("getTokenFromAuthService", () => {
|
|||||||
|
|
||||||
it("builds the correct URL in production", () => {
|
it("builds the correct URL in production", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
});
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
"https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL in dev", () => {
|
it("builds the correct URL in dev", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
});
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -79,7 +78,7 @@ describe("requestPlugin", () => {
|
|||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
platform: Platform.Hosted,
|
platform: Platform.Hosted,
|
||||||
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234",
|
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { AuthType } from "../AuthType";
|
|||||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { updateUserContext, userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -27,7 +27,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
);
|
);
|
||||||
if (!userContext.aadToken) {
|
if (!userContext.aadToken) {
|
||||||
logConsoleError(
|
logConsoleError(
|
||||||
`AAD token does not exist. Please use the "Login for Entra ID" button in the Toolbar prior to performing Entra ID RBAC operations`,
|
`AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -189,19 +189,10 @@ let _client: Cosmos.CosmosClient;
|
|||||||
|
|
||||||
export function client(): Cosmos.CosmosClient {
|
export function client(): Cosmos.CosmosClient {
|
||||||
if (_client) {
|
if (_client) {
|
||||||
if (!userContext.refreshCosmosClient) {
|
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
_client.dispose();
|
|
||||||
_client = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.refreshCosmosClient) {
|
|
||||||
updateUserContext({
|
|
||||||
refreshCosmosClient: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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,5 +1,3 @@
|
|||||||
import { TagNames, WorkloadType } from "Common/Constants";
|
|
||||||
import { Tags } from "Contracts/DataModels";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
function isVirtualNetworkFilterEnabled() {
|
function isVirtualNetworkFilterEnabled() {
|
||||||
@@ -17,12 +15,3 @@ function isPrivateEndpointConnectionsEnabled() {
|
|||||||
export function isPublicInternetAccessAllowed(): boolean {
|
export function isPublicInternetAccessAllowed(): boolean {
|
||||||
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export function getNewDatabaseSharedThroughputDefault(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ export interface TableEntityProps {
|
|||||||
isEntityValueDisable?: boolean;
|
isEntityValueDisable?: boolean;
|
||||||
entityTimeValue: string;
|
entityTimeValue: string;
|
||||||
entityValueType: string;
|
entityValueType: string;
|
||||||
entityProperty: string;
|
|
||||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
onSelectDate: (date: Date | null | undefined) => void;
|
onSelectDate: (date: Date | null | undefined) => void;
|
||||||
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
@@ -27,7 +26,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
onSelectDate,
|
onSelectDate,
|
||||||
isEntityValueDisable,
|
isEntityValueDisable,
|
||||||
onEntityTimeValueChange,
|
onEntityTimeValueChange,
|
||||||
entityProperty,
|
|
||||||
}: TableEntityProps): JSX.Element => {
|
}: TableEntityProps): JSX.Element => {
|
||||||
if (isEntityTypeDate) {
|
if (isEntityTypeDate) {
|
||||||
return (
|
return (
|
||||||
@@ -53,20 +51,15 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TextField
|
||||||
<span id={entityProperty} className="screenReaderOnly">
|
label={entityValueLabel && entityValueLabel}
|
||||||
Edit Property {entityProperty} {attributeValueLabel}
|
className="addEntityTextField"
|
||||||
</span>
|
disabled={isEntityValueDisable}
|
||||||
<TextField
|
type={entityValueType}
|
||||||
label={entityValueLabel && entityValueLabel}
|
placeholder={entityValuePlaceholder}
|
||||||
className="addEntityTextField"
|
value={typeof entityValue === "string" ? entityValue : ""}
|
||||||
disabled={isEntityValueDisable}
|
onChange={onEntityValueChange}
|
||||||
type={entityValueType}
|
ariaLabel={attributeValueLabel}
|
||||||
placeholder={entityValuePlaceholder}
|
/>
|
||||||
value={typeof entityValue === "string" ? entityValue : ""}
|
|
||||||
onChange={onEntityValueChange}
|
|
||||||
aria-labelledby={entityProperty}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { MongoProxyEndpoints } from "Common/Constants";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
@@ -72,8 +71,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -84,19 +82,16 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -108,8 +103,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -120,19 +114,16 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -144,8 +135,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -156,19 +146,16 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -180,8 +167,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -192,19 +178,16 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -216,8 +199,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -228,19 +210,16 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocument(databaseId, collection, documentId);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
|
||||||
deleteDocument(databaseId, collection, documentId);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
"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",
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -252,14 +231,13 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
it("returns a production endpoint", () => {
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a development endpoint", () => {
|
it("returns a development endpoint", () => {
|
||||||
@@ -271,20 +249,18 @@ describe("MongoProxyClient", () => {
|
|||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.EncryptedToken,
|
authType: AuthType.EncryptedToken,
|
||||||
});
|
});
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getFeatureEndpointOrDefault", () => {
|
describe("getFeatureEndpointOrDefault", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
});
|
});
|
||||||
const features = extractFeatures(params);
|
const features = extractFeatures(params);
|
||||||
@@ -296,12 +272,12 @@ describe("MongoProxyClient", () => {
|
|||||||
|
|
||||||
it("returns a local endpoint", () => {
|
it("returns a local endpoint", () => {
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
it("returns a production endpoint", () => {
|
||||||
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
defaultAllowedMongoProxyEndpoints,
|
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
import queryString from "querystring";
|
import queryString from "querystring";
|
||||||
@@ -14,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
|||||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -194,7 +194,7 @@ export function readDocument(
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
if (!useMongoProxyEndpoint("readDocument")) {
|
||||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -217,7 +217,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -289,7 +289,7 @@ export function createDocument(
|
|||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
if (!useMongoProxyEndpoint("createDocument")) {
|
||||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -308,7 +308,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -373,7 +373,7 @@ export function updateDocument(
|
|||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
if (!useMongoProxyEndpoint("updateDocument")) {
|
||||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -396,7 +396,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -464,7 +464,7 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) {
|
if (!useMongoProxyEndpoint("deleteDocument")) {
|
||||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -486,7 +486,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
|||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument);
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -561,10 +561,7 @@ export function deleteDocuments(
|
|||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
|
||||||
const rids: string[] = documentIds.map((documentId) => {
|
const rids = documentIds.map((documentId) => documentId.id());
|
||||||
const idComponents = documentId.self.split("/");
|
|
||||||
return idComponents[5];
|
|
||||||
});
|
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
databaseID: databaseId,
|
databaseID: databaseId,
|
||||||
@@ -575,7 +572,7 @@ export function deleteDocuments(
|
|||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -599,7 +596,7 @@ export function deleteDocuments(
|
|||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
||||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||||
}
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
@@ -622,7 +619,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -692,13 +689,12 @@ export function getFeatureEndpointOrDefault(feature: string): string {
|
|||||||
if (useMongoProxyEndpoint(feature)) {
|
if (useMongoProxyEndpoint(feature)) {
|
||||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||||
} else {
|
} else {
|
||||||
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
|
||||||
...defaultAllowedMongoProxyEndpoints,
|
|
||||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
];
|
|
||||||
endpoint =
|
endpoint =
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
||||||
|
...allowedMongoProxyEndpoints,
|
||||||
|
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
])
|
||||||
? userContext.features.mongoProxyEndpoint
|
? userContext.features.mongoProxyEndpoint
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -719,82 +715,19 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
export function useMongoProxyEndpoint(api: string): boolean {
|
||||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
const activeMongoProxyEndpoints: string[] = [
|
||||||
[MongoProxyApi.ResourceList]: [
|
MongoProxyEndpoints.Local,
|
||||||
MongoProxyEndpoints.Development,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Mooncake,
|
||||||
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,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
return (
|
||||||
return false;
|
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||||
}
|
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||||
|
);
|
||||||
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThrottlingError extends Error {
|
export class ThrottlingError extends Error {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe("QueryError.tryParse", () => {
|
|||||||
code: "BadRequest",
|
code: "BadRequest",
|
||||||
message: "Your query is bad, and you should feel bad",
|
message: "Your query is bad, and you should feel bad",
|
||||||
};
|
};
|
||||||
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
const message = JSON.stringify(innerError);
|
||||||
const outerError = {
|
const outerError = {
|
||||||
code: "BadRequest",
|
code: "BadRequest",
|
||||||
message,
|
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, along with a prefix and activity id.
|
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message.
|
||||||
it("handles single-nested error", () => {
|
it("handles single-nested error", () => {
|
||||||
const errors = [
|
const errors = [
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ describe("QueryError.tryParse", () => {
|
|||||||
message: "Your query is bad, and you should feel bad",
|
message: "Your query is bad, and you should feel bad",
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
const message = JSON.stringify(innerError);
|
||||||
const outerError = {
|
const outerError = {
|
||||||
code: "BadRequest",
|
code: "BadRequest",
|
||||||
message,
|
message,
|
||||||
@@ -91,23 +91,4 @@ 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,28 +214,16 @@ export default class QueryError {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some newer backends produce a message that contains a doubly-nested JSON payload.
|
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
||||||
// In this case, the message we get is a fully-complete JSON object we can parse.
|
if (message.startsWith("Message: ")) {
|
||||||
// So let's try that first
|
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
||||||
if (message.startsWith("{") && message.endsWith("}")) {
|
// So we use a separate variable to avoid this.
|
||||||
let outer: unknown = undefined;
|
message = message.substring("Message: ".length);
|
||||||
try {
|
|
||||||
outer = JSON.parse(message);
|
|
||||||
if (typeof outer === "object" && "message" in outer && typeof outer.message === "string") {
|
|
||||||
message = outer.message;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Just continue if the parsing fails. We'll use the fallback logic below.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = message.split("\n");
|
const lines = message.split("\n");
|
||||||
message = lines[0].trim();
|
message = lines[0].trim();
|
||||||
|
|
||||||
if (message.startsWith("Message: ")) {
|
|
||||||
message = message.substring("Message: ".length);
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(message);
|
parsed = JSON.parse(message);
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
onEntityValueChange={onEntityValueChange}
|
onEntityValueChange={onEntityValueChange}
|
||||||
onSelectDate={onSelectDate}
|
onSelectDate={onSelectDate}
|
||||||
onEntityTimeValueChange={onEntityTimeValueChange}
|
onEntityTimeValueChange={onEntityTimeValueChange}
|
||||||
entityProperty={entityProperty}
|
|
||||||
/>
|
/>
|
||||||
{!isEntityValueDisable && (
|
{!isEntityValueDisable && (
|
||||||
<TooltipHost content="Edit property" id="editTooltip">
|
<TooltipHost content="Edit property" id="editTooltip">
|
||||||
|
|||||||
@@ -99,9 +99,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
|||||||
if (params.vectorEmbeddingPolicy) {
|
if (params.vectorEmbeddingPolicy) {
|
||||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||||
}
|
}
|
||||||
if (params.fullTextPolicy) {
|
|
||||||
resource.fullTextPolicy = params.fullTextPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -273,7 +270,6 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
|||||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||||
analyticalStorageTtl: params.analyticalStorageTtl,
|
analyticalStorageTtl: params.analyticalStorageTtl,
|
||||||
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
||||||
fullTextPolicy: params.fullTextPolicy,
|
|
||||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||||
const collectionOptions: RequestOptions = {};
|
const collectionOptions: RequestOptions = {};
|
||||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
|
allowedCassandraProxyEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
defaultAllowedCassandraProxyEndpoints,
|
|
||||||
defaultAllowedMongoProxyEndpoints,
|
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
@@ -32,8 +32,6 @@ export interface ConfigContext {
|
|||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
|
||||||
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
|
||||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
@@ -55,6 +53,7 @@ export interface ConfigContext {
|
|||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
|
NEW_MONGO_APIS?: string[];
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
NEW_CASSANDRA_APIS?: string[];
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
@@ -67,8 +66,6 @@ export interface ConfigContext {
|
|||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
globallyEnabledCassandraAPIs?: string[];
|
|
||||||
globallyEnabledMongoAPIs?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
@@ -76,12 +73,9 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
|
||||||
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
|
||||||
allowedParentFrameOrigins: [
|
allowedParentFrameOrigins: [
|
||||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/cdb-(ms|ff|mc)-prod-pbe\\.cosmos\\.azure\\.(com|us|cn)$`,
|
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||||
@@ -91,7 +85,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
@@ -112,12 +106,21 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
NEW_MONGO_APIS: [
|
||||||
|
"resourcelist",
|
||||||
|
"queryDocuments",
|
||||||
|
"createDocument",
|
||||||
|
"readDocument",
|
||||||
|
"updateDocument",
|
||||||
|
"deleteDocument",
|
||||||
|
"createCollectionWithProxy",
|
||||||
|
"legacyMongoShell",
|
||||||
|
// "bulkdelete",
|
||||||
|
],
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
globallyEnabledCassandraAPIs: [],
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -161,12 +164,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
||||||
!validateEndpoint(
|
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
|
||||||
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,12 +172,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
||||||
!validateEndpoint(
|
|
||||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
|
||||||
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export enum TabKind {
|
|||||||
Graph,
|
Graph,
|
||||||
SQLQuery,
|
SQLQuery,
|
||||||
ScaleSettings,
|
ScaleSettings,
|
||||||
MongoQuery,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,8 +51,6 @@ export interface OpenCollectionTab extends OpenTab {
|
|||||||
*/
|
*/
|
||||||
export interface OpenQueryTab extends OpenCollectionTab {
|
export interface OpenQueryTab extends OpenCollectionTab {
|
||||||
query: QueryInfo;
|
query: QueryInfo;
|
||||||
splitterDirection?: "vertical" | "horizontal";
|
|
||||||
queryViewSizePercent?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export interface ArmEntity {
|
|||||||
location: string;
|
location: string;
|
||||||
type: string;
|
type: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
tags?: Tags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccount extends ArmEntity {
|
export interface DatabaseAccount extends ArmEntity {
|
||||||
@@ -160,7 +159,6 @@ export interface Collection extends Resource {
|
|||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
fullTextPolicy?: FullTextPolicy;
|
|
||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
computedProperties?: ComputedProperties;
|
computedProperties?: ComputedProperties;
|
||||||
@@ -201,19 +199,11 @@ export interface IndexingPolicy {
|
|||||||
compositeIndexes?: any[];
|
compositeIndexes?: any[];
|
||||||
spatialIndexes?: any[];
|
spatialIndexes?: any[];
|
||||||
vectorIndexes?: VectorIndex[];
|
vectorIndexes?: VectorIndex[];
|
||||||
fullTextIndexes?: FullTextIndex[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorIndex {
|
export interface VectorIndex {
|
||||||
path: string;
|
path: string;
|
||||||
type: "flat" | "diskANN" | "quantizedFlat";
|
type: "flat" | "diskANN" | "quantizedFlat";
|
||||||
diskANNShardKey?: string;
|
|
||||||
indexingSearchListSize?: number;
|
|
||||||
quantizationByteSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FullTextIndex {
|
|
||||||
path: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputedProperty {
|
export interface ComputedProperty {
|
||||||
@@ -352,7 +342,6 @@ export interface CreateCollectionParams {
|
|||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
createMongoWildcardIndex?: boolean;
|
createMongoWildcardIndex?: boolean;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
fullTextPolicy?: FullTextPolicy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicy {
|
export interface VectorEmbeddingPolicy {
|
||||||
@@ -366,16 +355,6 @@ export interface VectorEmbedding {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FullTextPolicy {
|
|
||||||
defaultLanguage: string;
|
|
||||||
fullTextPaths: FullTextPath[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FullTextPath {
|
|
||||||
path: string;
|
|
||||||
language: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReadDatabaseOfferParams {
|
export interface ReadDatabaseOfferParams {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseResourceId?: string;
|
databaseResourceId?: string;
|
||||||
@@ -664,5 +643,3 @@ export interface FeatureRegistration {
|
|||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tags = { [key: string]: string };
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export enum MessageTypes {
|
|||||||
OpenPostgreSQLPasswordReset,
|
OpenPostgreSQLPasswordReset,
|
||||||
OpenPostgresNetworkingBlade,
|
OpenPostgresNetworkingBlade,
|
||||||
OpenCosmosDBNetworkingBlade,
|
OpenCosmosDBNetworkingBlade,
|
||||||
DisplayNPSSurvey, // unused
|
DisplayNPSSurvey,
|
||||||
OpenVCoreMongoNetworkingBlade,
|
OpenVCoreMongoNetworkingBlade,
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
||||||
|
|||||||
@@ -115,13 +115,7 @@ export interface CollectionBase extends TreeNode {
|
|||||||
isSampleCollection?: boolean;
|
isSampleCollection?: boolean;
|
||||||
|
|
||||||
onDocumentDBDocumentsClick(): void;
|
onDocumentDBDocumentsClick(): void;
|
||||||
onNewQueryClick(
|
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||||
source: any,
|
|
||||||
event?: MouseEvent,
|
|
||||||
queryText?: string,
|
|
||||||
splitterDirection?: "horizontal" | "vertical",
|
|
||||||
queryViewSizePercent?: number,
|
|
||||||
): void;
|
|
||||||
expandCollection(): void;
|
expandCollection(): void;
|
||||||
collapseCollection(): void;
|
collapseCollection(): void;
|
||||||
getDatabase(): Database;
|
getDatabase(): Database;
|
||||||
@@ -132,8 +126,6 @@ export interface Collection extends CollectionBase {
|
|||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
schema?: DataModels.ISchema;
|
schema?: DataModels.ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
|
||||||
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
usageSizeInKB: ko.Observable<number>;
|
usageSizeInKB: ko.Observable<number>;
|
||||||
@@ -157,13 +149,7 @@ export interface Collection extends CollectionBase {
|
|||||||
onSettingsClick: () => Promise<void>;
|
onSettingsClick: () => Promise<void>;
|
||||||
|
|
||||||
onNewGraphClick(): void;
|
onNewGraphClick(): void;
|
||||||
onNewMongoQueryClick(
|
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||||
source: any,
|
|
||||||
event?: MouseEvent,
|
|
||||||
queryText?: string,
|
|
||||||
splitterDirection?: "horizontal" | "vertical",
|
|
||||||
queryViewSizePercent?: number,
|
|
||||||
): void;
|
|
||||||
onNewMongoShellClick(): void;
|
onNewMongoShellClick(): void;
|
||||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||||
@@ -323,8 +309,6 @@ export interface QueryTabOptions extends TabOptions {
|
|||||||
partitionKey?: DataModels.PartitionKey;
|
partitionKey?: DataModels.PartitionKey;
|
||||||
queryText?: string;
|
queryText?: string;
|
||||||
resourceTokenPartitionKey?: string;
|
resourceTokenPartitionKey?: string;
|
||||||
splitterDirection?: "horizontal" | "vertical";
|
|
||||||
queryViewSizePercent?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptTabOption extends TabOptions {
|
export interface ScriptTabOption extends TabOptions {
|
||||||
@@ -398,8 +382,6 @@ export interface DataExplorerInputsFrame {
|
|||||||
databaseAccount: any;
|
databaseAccount: any;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
resourceGroup?: string;
|
resourceGroup?: string;
|
||||||
tenantId?: string;
|
|
||||||
userName?: string;
|
|
||||||
masterKey?: string;
|
masterKey?: string;
|
||||||
hasWriteAccess?: boolean;
|
hasWriteAccess?: boolean;
|
||||||
authorizationToken?: string;
|
authorizationToken?: string;
|
||||||
|
|||||||
@@ -56,15 +56,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: () =>
|
||||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
useSidePanel
|
||||||
useSidePanel
|
.getState()
|
||||||
.getState()
|
.openSidePanel(
|
||||||
.openSidePanel(
|
"Delete " + getDatabaseName(),
|
||||||
"Delete " + getDatabaseName(),
|
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
label: `Delete ${getDatabaseName()}`,
|
label: `Delete ${getDatabaseName()}`,
|
||||||
styleClass: "deleteDatabaseMenuItem",
|
styleClass: "deleteDatabaseMenuItem",
|
||||||
});
|
});
|
||||||
@@ -148,15 +146,14 @@ export const createCollectionContextMenuButton = (
|
|||||||
if (configContext.platform !== Platform.Fabric) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
useSidePanel
|
||||||
useSidePanel
|
.getState()
|
||||||
.getState()
|
.openSidePanel(
|
||||||
.openSidePanel(
|
"Delete " + getCollectionName(),
|
||||||
"Delete " + getCollectionName(),
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
label: `Delete ${getCollectionName()}`,
|
label: `Delete ${getCollectionName()}`,
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react";
|
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
@@ -9,9 +9,6 @@ export interface CollapsibleSectionProps {
|
|||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||||
showDelete?: boolean;
|
|
||||||
onDelete?: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
@@ -72,20 +69,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
)}
|
)}
|
||||||
{this.props.showDelete && (
|
|
||||||
<Stack.Item style={{ marginLeft: "auto" }}>
|
|
||||||
<IconButton
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
id={`delete-${this.props.title.split(" ").join("-")}`}
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ height: 27, marginRight: "20px" }}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onDelete();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
{this.state.isExpanded && this.props.children}
|
{this.state.isExpanded && this.props.children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import * as StringUtils from "../../../Utils/StringUtils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for this component
|
* Options for this component
|
||||||
*/
|
*/
|
||||||
export interface CommandButtonComponentProps {
|
export interface CommandButtonComponentProps {
|
||||||
/**
|
|
||||||
* font icon name for the button
|
|
||||||
*/
|
|
||||||
iconName?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* image source for the button icon
|
* image source for the button icon
|
||||||
*/
|
*/
|
||||||
@@ -31,7 +22,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -120,157 +111,3 @@ export interface CommandButtonComponentProps {
|
|||||||
*/
|
*/
|
||||||
keyboardAction?: KeyboardAction;
|
keyboardAction?: KeyboardAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
|
||||||
private dropdownElt: HTMLElement;
|
|
||||||
private expandButtonElt: HTMLElement;
|
|
||||||
|
|
||||||
public componentDidUpdate(): void {
|
|
||||||
if (!this.dropdownElt || !this.expandButtonElt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
|
||||||
}
|
|
||||||
|
|
||||||
private onKeyPress(event: React.KeyboardEvent): boolean {
|
|
||||||
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
|
||||||
this.commandClickCallback && this.commandClickCallback(event);
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
|
|
||||||
if (event.keyCode === KeyCodes.DownArrow) {
|
|
||||||
$(this.dropdownElt).hide();
|
|
||||||
$(this.dropdownElt).show().focus();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (event.keyCode === KeyCodes.UpArrow) {
|
|
||||||
$(this.dropdownElt).hide();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCommandButtonId(): string {
|
|
||||||
if (this.props.id) {
|
|
||||||
return this.props.id;
|
|
||||||
} else {
|
|
||||||
return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element {
|
|
||||||
return <CommandButtonComponent key={key} {...options} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
private commandClickCallback(e: React.SyntheticEvent): void {
|
|
||||||
if (this.props.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Query component's parent, not document
|
|
||||||
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
|
|
||||||
if (el) {
|
|
||||||
el.style.display = "none";
|
|
||||||
}
|
|
||||||
this.props.onCommandClick(e);
|
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
|
||||||
commandButtonClicked: this.props.commandButtonLabel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderChildren(): JSX.Element {
|
|
||||||
if (!this.props.children || this.props.children.length < 1) {
|
|
||||||
return <React.Fragment />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="commandExpand"
|
|
||||||
tabIndex={0}
|
|
||||||
ref={(ref: HTMLElement) => {
|
|
||||||
this.expandButtonElt = ref;
|
|
||||||
}}
|
|
||||||
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
|
|
||||||
>
|
|
||||||
<div className="commandDropdownLauncher">
|
|
||||||
<span className="partialSplitter" />
|
|
||||||
<span className="expandDropdown">
|
|
||||||
<img src={CollapseChevronDownIcon} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="commandDropdownContainer"
|
|
||||||
ref={(ref: HTMLElement) => {
|
|
||||||
this.dropdownElt = ref;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="commandDropdown">
|
|
||||||
{this.props.children.map((c: CommandButtonComponentProps, index: number): JSX.Element => {
|
|
||||||
return CommandButtonComponent.renderButton(c, `${index}`);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static renderLabel(
|
|
||||||
props: CommandButtonComponentProps,
|
|
||||||
key?: string,
|
|
||||||
refct?: (input: HTMLElement) => void,
|
|
||||||
): JSX.Element {
|
|
||||||
if (!props.commandButtonLabel) {
|
|
||||||
return <React.Fragment />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="commandLabel" key={key} ref={refct}>
|
|
||||||
{props.commandButtonLabel}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
let mainClassName = "commandButtonComponent";
|
|
||||||
if (this.props.disabled) {
|
|
||||||
mainClassName += " commandDisabled";
|
|
||||||
}
|
|
||||||
if (this.props.isSelected) {
|
|
||||||
mainClassName += " selectedButton";
|
|
||||||
}
|
|
||||||
|
|
||||||
let contentClassName = "commandContent";
|
|
||||||
if (this.props.children && this.props.children.length > 0) {
|
|
||||||
contentClassName += " hasHiddenItems";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="commandButtonReact">
|
|
||||||
<span
|
|
||||||
className={mainClassName}
|
|
||||||
role="menuitem"
|
|
||||||
tabIndex={this.props.tabIndex}
|
|
||||||
onKeyPress={(e: React.KeyboardEvent<HTMLSpanElement>) => this.onKeyPress(e)}
|
|
||||||
title={this.props.tooltipText}
|
|
||||||
id={this.getCommandButtonId()}
|
|
||||||
aria-disabled={this.props.disabled}
|
|
||||||
aria-haspopup={this.props.hasPopup}
|
|
||||||
aria-label={this.props.ariaLabel}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
|
|
||||||
>
|
|
||||||
<div className={contentClassName}>
|
|
||||||
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
|
|
||||||
{CommandButtonComponent.renderLabel(this.props)}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
{this.props.children && this.renderChildren()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
|
|
||||||
describe("AddFullTextPolicyForm", () => {
|
|
||||||
//CTODO: add tests
|
|
||||||
it.skip("should render correctly", () => {});
|
|
||||||
});
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
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)",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
// 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,12 @@ import {
|
|||||||
ComputedPropertiesComponentProps,
|
ComputedPropertiesComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||||
import {
|
import {
|
||||||
ContainerPolicyComponent,
|
ContainerVectorPolicyComponent,
|
||||||
ContainerPolicyComponentProps,
|
ContainerVectorPolicyComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
@@ -32,7 +33,6 @@ import {
|
|||||||
PartitionKeyComponent,
|
PartitionKeyComponent,
|
||||||
PartitionKeyComponentProps,
|
PartitionKeyComponentProps,
|
||||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
@@ -105,13 +105,6 @@ export interface SettingsComponentState {
|
|||||||
isSubSettingsSaveable: boolean;
|
isSubSettingsSaveable: boolean;
|
||||||
isSubSettingsDiscardable: boolean;
|
isSubSettingsDiscardable: boolean;
|
||||||
|
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
|
||||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
|
||||||
fullTextPolicy: DataModels.FullTextPolicy;
|
|
||||||
fullTextPolicyBaseline: DataModels.FullTextPolicy;
|
|
||||||
shouldDiscardContainerPolicies: boolean;
|
|
||||||
isContainerPolicyDirty: boolean;
|
|
||||||
|
|
||||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||||
shouldDiscardIndexingPolicy: boolean;
|
shouldDiscardIndexingPolicy: boolean;
|
||||||
@@ -156,7 +149,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
@@ -172,7 +164,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
|
||||||
@@ -212,13 +203,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
|
|
||||||
vectorEmbeddingPolicy: undefined,
|
|
||||||
vectorEmbeddingPolicyBaseline: undefined,
|
|
||||||
fullTextPolicy: undefined,
|
|
||||||
fullTextPolicyBaseline: undefined,
|
|
||||||
shouldDiscardContainerPolicies: false,
|
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
|
|
||||||
indexingPolicyContent: undefined,
|
indexingPolicyContent: undefined,
|
||||||
indexingPolicyContentBaseline: undefined,
|
indexingPolicyContentBaseline: undefined,
|
||||||
shouldDiscardIndexingPolicy: false,
|
shouldDiscardIndexingPolicy: false,
|
||||||
@@ -323,7 +307,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return (
|
return (
|
||||||
this.state.isScaleSaveable ||
|
this.state.isScaleSaveable ||
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
this.state.isContainerPolicyDirty ||
|
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
this.state.isComputedPropertiesDirty ||
|
||||||
@@ -335,7 +318,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return (
|
return (
|
||||||
this.state.isScaleDiscardable ||
|
this.state.isScaleDiscardable ||
|
||||||
this.state.isSubSettingsDiscardable ||
|
this.state.isSubSettingsDiscardable ||
|
||||||
this.state.isContainerPolicyDirty ||
|
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
this.state.isComputedPropertiesDirty ||
|
||||||
@@ -423,8 +405,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||||
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
||||||
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicyBaseline,
|
|
||||||
fullTextPolicy: this.state.fullTextPolicyBaseline,
|
|
||||||
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
||||||
indexesToAdd: [],
|
indexesToAdd: [],
|
||||||
indexesToDrop: [],
|
indexesToDrop: [],
|
||||||
@@ -436,13 +416,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
||||||
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
||||||
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
||||||
shouldDiscardContainerPolicies: true,
|
|
||||||
shouldDiscardIndexingPolicy: true,
|
shouldDiscardIndexingPolicy: true,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isMongoIndexingPolicySaveable: false,
|
isMongoIndexingPolicySaveable: false,
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
@@ -470,17 +448,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
||||||
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
||||||
|
|
||||||
private onVectorEmbeddingPolicyChange = (newVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy): void =>
|
|
||||||
this.setState({ vectorEmbeddingPolicy: newVectorEmbeddingPolicy });
|
|
||||||
|
|
||||||
private onFullTextPolicyChange = (newFullTextPolicy: DataModels.FullTextPolicy): void =>
|
|
||||||
this.setState({ fullTextPolicy: newFullTextPolicy });
|
|
||||||
|
|
||||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||||
|
|
||||||
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
|
||||||
|
|
||||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||||
|
|
||||||
private logIndexingPolicySuccessMessage = (): void => {
|
private logIndexingPolicySuccessMessage = (): void => {
|
||||||
@@ -568,12 +538,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
||||||
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
||||||
|
|
||||||
private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void =>
|
|
||||||
this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty });
|
|
||||||
|
|
||||||
private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void =>
|
|
||||||
this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty });
|
|
||||||
|
|
||||||
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
||||||
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
||||||
|
|
||||||
@@ -727,10 +691,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
const vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy =
|
|
||||||
this.collection.vectorEmbeddingPolicy && this.collection.vectorEmbeddingPolicy();
|
|
||||||
const fullTextPolicy: DataModels.FullTextPolicy =
|
|
||||||
this.collection.fullTextPolicy && this.collection.fullTextPolicy();
|
|
||||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||||
@@ -764,10 +724,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||||
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
||||||
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
|
|
||||||
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
|
|
||||||
fullTextPolicy: fullTextPolicy,
|
|
||||||
fullTextPolicyBaseline: fullTextPolicy,
|
|
||||||
indexingPolicyContent: indexingPolicyContent,
|
indexingPolicyContent: indexingPolicyContent,
|
||||||
indexingPolicyContentBaseline: indexingPolicyContent,
|
indexingPolicyContentBaseline: indexingPolicyContent,
|
||||||
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
||||||
@@ -898,7 +854,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
this.state.isContainerPolicyDirty ||
|
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty
|
this.state.isComputedPropertiesDirty
|
||||||
@@ -920,10 +875,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||||
newCollection.defaultTtl = defaultTtl;
|
newCollection.defaultTtl = defaultTtl;
|
||||||
|
|
||||||
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
|
|
||||||
|
|
||||||
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
|
||||||
|
|
||||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||||
|
|
||||||
newCollection.changeFeedPolicy =
|
newCollection.changeFeedPolicy =
|
||||||
@@ -962,8 +913,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||||
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
|
|
||||||
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
|
|
||||||
|
|
||||||
if (wasIndexingPolicyModified) {
|
if (wasIndexingPolicyModified) {
|
||||||
await this.refreshIndexTransformationProgress();
|
await this.refreshIndexTransformationProgress();
|
||||||
@@ -972,7 +921,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({
|
this.setState({
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
isComputedPropertiesDirty: false,
|
isComputedPropertiesDirty: false,
|
||||||
@@ -1143,21 +1091,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerPolicyComponentProps: ContainerPolicyComponentProps = {
|
|
||||||
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicy,
|
|
||||||
vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline,
|
|
||||||
onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange,
|
|
||||||
onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange,
|
|
||||||
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
|
||||||
fullTextPolicy: this.state.fullTextPolicy,
|
|
||||||
fullTextPolicyBaseline: this.state.fullTextPolicyBaseline,
|
|
||||||
onFullTextPolicyChange: this.onFullTextPolicyChange,
|
|
||||||
onFullTextPolicyDirtyChange: this.onFullTextPolicyDirtyChange,
|
|
||||||
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
|
||||||
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
|
||||||
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
|
||||||
};
|
|
||||||
|
|
||||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||||
@@ -1215,6 +1148,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
||||||
|
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1228,10 +1165,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) {
|
if (this.isVectorSearchEnabled) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||||
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />,
|
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
|
|
||||||
describe("ContainerPolicyComponent", () => {
|
|
||||||
//CTODO: add tests
|
|
||||||
it.skip("should render correctly", () => {});
|
|
||||||
});
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Stack } from "@fluentui/react";
|
||||||
|
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
|
||||||
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface ContainerVectorPolicyComponentProps {
|
||||||
|
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
|
||||||
|
isReadOnly={true}
|
||||||
|
wordWrap={"on"}
|
||||||
|
ariaLabel={"Container vector policy"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
scrollBeyondLastLine={false}
|
||||||
|
className={"settingsV2Editor"}
|
||||||
|
spinnerClassName={"settingsV2EditorSpinner"}
|
||||||
|
fontSize={14}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -120,6 +120,11 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||||
/>
|
/>
|
||||||
|
{this.props.isVectorSearchEnabled && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
||||||
|
Container vector policies and vector indexes are not modifiable after container creation
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
|
|||||||
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||||
import {
|
import {
|
||||||
@@ -178,14 +177,12 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
To change the partition key, a new destination container must be created or an existing destination container
|
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.
|
selected. Data will then be copied to the destination container.
|
||||||
</Text>
|
</Text>
|
||||||
{configContext.platform !== Platform.Emulator && (
|
<PrimaryButton
|
||||||
<PrimaryButton
|
styles={{ root: { width: "fit-content" } }}
|
||||||
styles={{ root: { width: "fit-content" } }}
|
text="Change"
|
||||||
text="Change"
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{portalDataTransferJob && (
|
{portalDataTransferJob && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||||
|
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../../../UserContext";
|
import { userContext } from "../../../../../UserContext";
|
||||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||||
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||||
import {
|
import {
|
||||||
PriceBreakdown,
|
PriceBreakdown,
|
||||||
@@ -365,6 +366,29 @@ 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 => {
|
private renderThroughputModeChoices = (): JSX.Element => {
|
||||||
const labelId = "settingsV2RadioButtonLabelId";
|
const labelId = "settingsV2RadioButtonLabelId";
|
||||||
return (
|
return (
|
||||||
@@ -637,6 +661,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
{this.minRUperGBSurvey()}
|
||||||
{this.props.spendAckVisible && (
|
{this.props.spendAckVisible && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="spendAckCheckBox"
|
id="spendAckCheckBox"
|
||||||
|
|||||||
@@ -4,14 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
export type isDirtyTypes =
|
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
||||||
| boolean
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| DataModels.IndexingPolicy
|
|
||||||
| DataModels.ComputedProperties
|
|
||||||
| DataModels.VectorEmbedding[]
|
|
||||||
| DataModels.FullTextPolicy;
|
|
||||||
export const TtlOff = "off";
|
export const TtlOff = "off";
|
||||||
export const TtlOn = "on";
|
export const TtlOn = "on";
|
||||||
export const TtlOnNoDefault = "on-nodefault";
|
export const TtlOnNoDefault = "on-nodefault";
|
||||||
@@ -57,11 +50,6 @@ export enum SettingsV2TabTypes {
|
|||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
|
||||||
VectorPolicyTab,
|
|
||||||
FullTextPolicyTab,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
isSaveable: boolean;
|
isSaveable: boolean;
|
||||||
isDiscardable: boolean;
|
isDiscardable: boolean;
|
||||||
@@ -166,7 +154,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||||
return "Computed Properties";
|
return "Computed Properties";
|
||||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||||
return "Container Policies";
|
return "Container Vector Policy (preview)";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ export const collection = {
|
|||||||
query: "query",
|
query: "query",
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
|
||||||
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
"fullTextPolicy": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
"geospatialConfig": [Function],
|
||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
@@ -72,7 +71,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
@@ -134,7 +132,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
"fullTextPolicy": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
"geospatialConfig": [Function],
|
||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
@@ -151,7 +148,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
displayedTtlSeconds="5"
|
displayedTtlSeconds="5"
|
||||||
@@ -253,7 +249,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"databaseId": "test",
|
"databaseId": "test",
|
||||||
"defaultTtl": [Function],
|
"defaultTtl": [Function],
|
||||||
"fullTextPolicy": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
"geospatialConfig": [Function],
|
||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
@@ -270,7 +265,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
@@ -35,15 +34,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: 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 [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
const [throughput, setThroughput] = useState<number>(
|
||||||
|
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
|
||||||
|
);
|
||||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||||
const [throughputError, setThroughputError] = useState<string>("");
|
const [throughputError, setThroughputError] = useState<string>("");
|
||||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||||
@@ -53,6 +47,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
|
|
||||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// throughput cap check for the initial state
|
// throughput cap check for the initial state
|
||||||
let totalThroughput = 0;
|
let totalThroughput = 0;
|
||||||
@@ -162,6 +157,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
|
|
||||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||||
if (mode === "Autoscale") {
|
if (mode === "Autoscale") {
|
||||||
|
const defaultThroughput = isFreeTier
|
||||||
|
? AutoPilotUtils.autoPilotThroughput1K
|
||||||
|
: AutoPilotUtils.autoPilotThroughput4K;
|
||||||
setThroughput(defaultThroughput);
|
setThroughput(defaultThroughput);
|
||||||
setIsAutoScaleSelected(true);
|
setIsAutoScaleSelected(true);
|
||||||
setThroughputValue(defaultThroughput);
|
setThroughputValue(defaultThroughput);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { useCallback } from "react";
|
|||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: (value?: React.RefObject<HTMLElement>) => void;
|
onClick: () => void;
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
@@ -74,7 +74,6 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
openItems,
|
openItems,
|
||||||
}: TreeNodeComponentProps): JSX.Element => {
|
}: TreeNodeComponentProps): JSX.Element => {
|
||||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||||
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
|
|
||||||
const treeStyles = useTreeStyles();
|
const treeStyles = useTreeStyles();
|
||||||
|
|
||||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
||||||
@@ -108,7 +107,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
|
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
||||||
if (data.type === "Click" && !isBranch && node.onClick) {
|
if (data.type === "Click" && node.onClick) {
|
||||||
node.onClick();
|
node.onClick();
|
||||||
}
|
}
|
||||||
if (!node.isExpanded && data.open && node.onExpanded) {
|
if (!node.isExpanded && data.open && node.onExpanded) {
|
||||||
@@ -120,7 +119,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
node.onCollapsed?.();
|
node.onCollapsed?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isBranch, node, setIsLoading],
|
[node, setIsLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuOpenChange = useCallback(
|
const onMenuOpenChange = useCallback(
|
||||||
@@ -142,7 +141,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
||||||
disabled={menuItem.isDisabled}
|
disabled={menuItem.isDisabled}
|
||||||
key={menuItem.label}
|
key={menuItem.label}
|
||||||
onClick={() => menuItem.onClick(contextMenuRef)}
|
onClick={menuItem.onClick}
|
||||||
>
|
>
|
||||||
{menuItem.label}
|
{menuItem.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -191,7 +190,6 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
||||||
data-test="TreeNode/ContextMenuTrigger"
|
data-test="TreeNode/ContextMenuTrigger"
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
ref={contextMenuRef}
|
|
||||||
icon={<MoreHorizontal20Regular />}
|
icon={<MoreHorizontal20Regular />}
|
||||||
/>
|
/>
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
|
|||||||
@@ -1478,14 +1478,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||||
onClick={[Function]}
|
onClick={[MockFunction enabledItemClick]}
|
||||||
>
|
>
|
||||||
enabledItem
|
enabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
onClick={[Function]}
|
onClick={[MockFunction disabledItemClick]}
|
||||||
>
|
>
|
||||||
disabledItem
|
disabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -1518,7 +1518,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||||
key="enabledItem"
|
key="enabledItem"
|
||||||
onClick={[Function]}
|
onClick={[MockFunction enabledItemClick]}
|
||||||
>
|
>
|
||||||
enabledItem
|
enabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -1526,7 +1526,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
key="disabledItem"
|
key="disabledItem"
|
||||||
onClick={[Function]}
|
onClick={[MockFunction disabledItemClick]}
|
||||||
>
|
>
|
||||||
disabledItem
|
disabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -1,470 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -5,12 +5,13 @@ import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
|||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -35,7 +36,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
|||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { updateUserContext, userContext } from "../UserContext";
|
import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
@@ -47,7 +48,6 @@ import { useTabs } from "../hooks/useTabs";
|
|||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
@@ -259,8 +259,25 @@ export default class Explorer {
|
|||||||
|
|
||||||
public async openLoginForEntraIDPopUp(): Promise<void> {
|
public async openLoginForEntraIDPopUp(): Promise<void> {
|
||||||
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
||||||
|
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||||
|
/\/$/,
|
||||||
|
"/.default",
|
||||||
|
);
|
||||||
|
const msalInstance = await getMsalInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false);
|
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")}`,
|
||||||
|
});
|
||||||
updateUserContext({ aadToken: aadToken });
|
updateUserContext({ aadToken: aadToken });
|
||||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -278,6 +295,37 @@ 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> {
|
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
@@ -1103,7 +1151,7 @@ export default class Explorer {
|
|||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
: this.refreshAllDatabases();
|
||||||
}
|
}
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
|
||||||
|
|||||||
@@ -4,83 +4,51 @@
|
|||||||
* and update any knockout observables passed from the parent.
|
* and update any knockout observables passed from the parent.
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
|
import {
|
||||||
|
createPlatformButtons,
|
||||||
|
createStaticCommandBarButtons,
|
||||||
|
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
|
||||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandBarStore {
|
|
||||||
contextButtons: CommandButtonComponentProps[];
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
|
||||||
isHidden: boolean;
|
|
||||||
setIsHidden: (isHidden: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
|
||||||
contextButtons: [],
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
|
||||||
isHidden: false,
|
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const selectedNodeState = useSelectedNode();
|
const selectedNodeState = useSelectedNode();
|
||||||
const buttons = useCommandBar((state) => state.contextButtons);
|
const contextButtons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
|
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
||||||
|
const platformButtons = createPlatformButtons();
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container);
|
||||||
const buttons =
|
if (contextButtons?.length > 0) {
|
||||||
userContext.apiType === "Postgres"
|
|
||||||
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
|
||||||
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
|
||||||
return (
|
|
||||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
|
||||||
<FluentCommandBar
|
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
|
||||||
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
|
||||||
styles={{
|
|
||||||
root: { backgroundColor: backgroundColor },
|
|
||||||
}}
|
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
|
||||||
const contextButtons = (buttons || []).concat(
|
|
||||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
|
||||||
);
|
|
||||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
|
||||||
|
|
||||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
|
||||||
if (buttons && buttons.length > 0) {
|
|
||||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(
|
||||||
|
contextButtons || [],
|
||||||
|
backgroundColor,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container);
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
|
||||||
@@ -88,7 +56,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
||||||
connectionInfo?.status !== ConnectionStatusType.Connect
|
connectionInfo?.status !== ConnectionStatusType.Connect
|
||||||
) {
|
) {
|
||||||
uiFabricControlButtons.unshift(
|
uiFabricPlatformButtons.unshift(
|
||||||
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,8 +75,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container);
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -116,7 +84,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
farItems={uiFabricControlButtons}
|
farItems={uiFabricPlatformButtons}
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ import { AuthType } from "../../../AuthType";
|
|||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
|
|
||||||
describe("CommandBarComponentButtonFactory tests", () => {
|
describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
let mockExplorer: Explorer;
|
|
||||||
|
|
||||||
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
||||||
|
|
||||||
describe("Enable Azure Synapse Link Button", () => {
|
describe("Enable Azure Synapse Link Button", () => {
|
||||||
@@ -19,7 +16,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -30,7 +26,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Button should be visible", () => {
|
it("Button should be visible", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -46,7 +42,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -62,7 +58,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -75,7 +71,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -108,7 +103,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -118,13 +113,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
portalEnv: "mooncake",
|
portalEnv: "mooncake",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -134,12 +129,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const openPostgresShellButtonLabel = "Open PSQL shell";
|
const openPostgresShellButtonLabel = "Open PSQL shell";
|
||||||
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates Postgres shell button", () => {
|
it("creates Postgres shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
|
const buttons = CommandBarComponentButtonFactory.createPostgreButtons();
|
||||||
const openPostgresShellButton = buttons.find(
|
const openPostgresShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -147,7 +138,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates vCore Mongo shell button", () => {
|
it("creates vCore Mongo shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
|
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons();
|
||||||
const openVCoreMongoShellButton = buttons.find(
|
const openVCoreMongoShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -162,8 +153,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.ResourceToken,
|
authType: AuthType.ResourceToken,
|
||||||
});
|
});
|
||||||
@@ -175,7 +164,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
kind: "DocumentDB",
|
kind: "DocumentDB",
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(2);
|
||||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||||
expect(buttons[0].disabled).toBe(false);
|
expect(buttons[0].disabled).toBe(false);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { userContext } from "../../../UserContext";
|
|||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
@@ -32,19 +31,20 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
|||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export function createStaticCommandBarButtons(
|
export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
||||||
container: Explorer,
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
selectedNodeState: SelectedNodeState,
|
return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons();
|
||||||
): CommandButtonComponentProps[] {
|
}
|
||||||
|
|
||||||
if (userContext.authType === AuthType.ResourceToken) {
|
if (userContext.authType === AuthType.ResourceToken) {
|
||||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(selectedNodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
// Avoid starting with a divider
|
// Avoid starting with a divider
|
||||||
const addDivider = () => {
|
const addDivider = () => {
|
||||||
if (buttons.length > 0) {
|
if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) {
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export function createStaticCommandBarButtons(
|
|||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables" &&
|
||||||
userContext.apiType !== "Cassandra"
|
userContext.apiType !== "Cassandra"
|
||||||
) {
|
) {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
const addSynapseLink = createOpenSynapseLinkDialogButton();
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(addSynapseLink);
|
buttons.push(addSynapseLink);
|
||||||
@@ -67,9 +67,9 @@ export function createStaticCommandBarButtons(
|
|||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buttonProps = createLoginForEntraIDButton(container);
|
const buttonProps = createLoginForEntraIDButton();
|
||||||
setLoginButtonProps(buttonProps);
|
setLoginButtonProps(buttonProps);
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
}, [dataPlaneRbacEnabled, aadTokenUpdated]);
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
@@ -87,8 +87,8 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
||||||
const openQueryBtn = createOpenQueryButton(container);
|
const openQueryBtn = createOpenQueryButton();
|
||||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +103,7 @@ export function createStaticCommandBarButtons(
|
|||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
|
tooltipText: userContext.features.commandBarV2 ? "New..." : label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -115,21 +116,12 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createContextCommandBarButtons(
|
|
||||||
container: Explorer,
|
|
||||||
selectedNodeState: SelectedNodeState,
|
|
||||||
): CommandButtonComponentProps[] {
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: (_, container) => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
@@ -141,6 +133,7 @@ export function createContextCommandBarButtons(
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
|
addDivider();
|
||||||
buttons.push(newMongoShellBtn);
|
buttons.push(newMongoShellBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,25 +146,27 @@ export function createContextCommandBarButtons(
|
|||||||
const newCassandraShellButton: CommandButtonComponentProps = {
|
const newCassandraShellButton: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: (_, container) => {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
|
addDivider();
|
||||||
buttons.push(newCassandraShellButton);
|
buttons.push(newCassandraShellButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createPlatformButtons(): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] = [
|
||||||
{
|
{
|
||||||
iconSrc: SettingsIcon,
|
iconSrc: SettingsIcon,
|
||||||
iconAlt: "Settings",
|
iconAlt: "Settings",
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
onCommandClick: (_, container) =>
|
||||||
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: "Settings",
|
ariaLabel: "Settings",
|
||||||
tooltipText: "Settings",
|
tooltipText: "Settings",
|
||||||
@@ -207,7 +202,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
|||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: label,
|
tooltipText: label,
|
||||||
@@ -239,7 +234,7 @@ function areScriptsSupported(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -257,7 +252,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
return {
|
return {
|
||||||
iconSrc: SynapseIcon,
|
iconSrc: SynapseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -266,12 +261,12 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
function createLoginForEntraIDButton(): CommandButtonComponentProps {
|
||||||
if (configContext.platform !== Platform.Portal) {
|
if (configContext.platform !== Platform.Portal) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommandClick = async () => {
|
const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => {
|
||||||
await container.openLoginForEntraIDPopUp();
|
await container.openLoginForEntraIDPopUp();
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
};
|
};
|
||||||
@@ -398,13 +393,14 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
function createOpenQueryButton(): CommandButtonComponentProps {
|
||||||
const label = "Open Query";
|
const label = "Open Query";
|
||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
tooltipText: userContext.features.commandBarV2 ? "Open Query..." : "Open Query",
|
||||||
keyboardAction: KeyboardAction.OPEN_QUERY,
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: () =>
|
onCommandClick: (_, container) =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@@ -427,10 +423,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenTerminalButtonByKind(
|
function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps {
|
||||||
container: Explorer,
|
|
||||||
terminalKind: ViewModels.TerminalKind,
|
|
||||||
): CommandButtonComponentProps {
|
|
||||||
const terminalFriendlyName = (): string => {
|
const terminalFriendlyName = (): string => {
|
||||||
switch (terminalKind) {
|
switch (terminalKind) {
|
||||||
case ViewModels.TerminalKind.Cassandra:
|
case ViewModels.TerminalKind.Cassandra:
|
||||||
@@ -453,7 +446,7 @@ function createOpenTerminalButtonByKind(
|
|||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: (_, container) => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(terminalKind);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
@@ -467,11 +460,10 @@ function createOpenTerminalButtonByKind(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
container: Explorer,
|
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
): CommandButtonComponentProps[] {
|
): CommandButtonComponentProps[] {
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
const openQueryBtn = createOpenQueryButton(container);
|
const openQueryBtn = createOpenQueryButton();
|
||||||
|
|
||||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||||
const isResourceTokenCollectionNodeSelected: boolean =
|
const isResourceTokenCollectionNodeSelected: boolean =
|
||||||
@@ -484,20 +476,20 @@ function createStaticCommandBarButtonsForResourceToken(
|
|||||||
|
|
||||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||||
if (!openQueryBtn.disabled) {
|
if (!openQueryBtn.disabled) {
|
||||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [newSqlQueryBtn, openQueryBtn];
|
return [newSqlQueryBtn, openQueryBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createPostgreButtons(): CommandButtonComponentProps[] {
|
||||||
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres);
|
||||||
|
|
||||||
return [openPostgreShellBtn];
|
return [openPostgreShellBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createVCoreMongoButtons(): CommandButtonComponentProps[] {
|
||||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo);
|
||||||
|
|
||||||
return [openVCoreMongoTerminalButton];
|
return [openVCoreMongoTerminalButton];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { ICommandBarItemProps } from "@fluentui/react";
|
import { ICommandBarItemProps } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
describe("CommandBarUtil tests", () => {
|
describe("CommandBarUtil tests", () => {
|
||||||
|
const mockExplorer = {} as Explorer;
|
||||||
const createButton = (): CommandButtonComponentProps => {
|
const createButton = (): CommandButtonComponentProps => {
|
||||||
return {
|
return {
|
||||||
iconSrc: "icon",
|
iconSrc: "icon",
|
||||||
@@ -22,7 +24,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const btn = createButton();
|
const btn = createButton();
|
||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
|
const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer);
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(undefined);
|
expect(converted.split).toBe(undefined);
|
||||||
@@ -46,7 +48,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btn.children.push(child);
|
btn.children.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
|
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer);
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(true);
|
expect(converted.split).toBe(true);
|
||||||
@@ -62,7 +64,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btns.push(createButton());
|
btns.push(createButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
|
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer);
|
||||||
const uniqueKeys = converteds
|
const uniqueKeys = converteds
|
||||||
.map((btn: ICommandBarItemProps) => btn.key)
|
.map((btn: ICommandBarItemProps) => btn.key)
|
||||||
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
||||||
@@ -74,10 +76,10 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
btn.commandButtonLabel = undefined;
|
btn.commandButtonLabel = undefined;
|
||||||
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
|
|
||||||
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
||||||
delete btn.commandButtonLabel;
|
delete btn.commandButtonLabel;
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
|||||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||||
* @param btns
|
* @param btns
|
||||||
*/
|
*/
|
||||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
export const convertButton = (
|
||||||
|
btns: CommandButtonComponentProps[],
|
||||||
|
backgroundColor: string,
|
||||||
|
container: Explorer,
|
||||||
|
): ICommandBarItemProps[] => {
|
||||||
const buttonHeightPx =
|
const buttonHeightPx =
|
||||||
configContext.platform == Platform.Fabric
|
configContext.platform == Platform.Fabric
|
||||||
? StyleConstants.FabricCommandBarButtonHeight
|
? StyleConstants.FabricCommandBarButtonHeight
|
||||||
@@ -54,15 +58,14 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth, // 16
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: btn.iconName ? "baseline" : undefined,
|
alignSelf: undefined,
|
||||||
filter: getFilter(btn.disabled),
|
filter: getFilter(btn.disabled),
|
||||||
},
|
},
|
||||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||||
iconName: btn.iconName,
|
|
||||||
},
|
},
|
||||||
onClick: btn.onCommandClick
|
onClick: btn.onCommandClick
|
||||||
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||||
btn.onCommandClick(ev);
|
btn.onCommandClick(ev, container);
|
||||||
let copilotEnabled = false;
|
let copilotEnabled = false;
|
||||||
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||||
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||||
@@ -135,7 +138,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
result.split = true;
|
result.split = true;
|
||||||
|
|
||||||
result.subMenuProps = {
|
result.subMenuProps = {
|
||||||
items: convertButton(btn.children, backgroundColor),
|
items: convertButton(btn.children, backgroundColor, container),
|
||||||
styles: {
|
styles: {
|
||||||
list: {
|
list: {
|
||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||||
@@ -186,7 +189,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
option?: IDropdownOption,
|
option?: IDropdownOption,
|
||||||
index?: number,
|
index?: number,
|
||||||
): void => {
|
): void => {
|
||||||
btn.children[index].onCommandClick(event);
|
btn.children[index].onCommandClick(event, container);
|
||||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,14 +240,17 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
export function createKeyboardHandlers(
|
||||||
|
allButtons: CommandButtonComponentProps[],
|
||||||
|
container: Explorer,
|
||||||
|
): KeyboardHandlerMap {
|
||||||
const handlers: KeyboardHandlerMap = {};
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
if (!button.disabled && button.keyboardAction) {
|
if (!button.disabled && button.keyboardAction) {
|
||||||
handlers[button.keyboardAction] = (e) => {
|
handlers[button.keyboardAction] = (e) => {
|
||||||
button.onCommandClick(e);
|
button.onCommandClick(e, container);
|
||||||
|
|
||||||
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
16
src/Explorer/Menus/CommandBar/useCommandBar.ts
Normal file
16
src/Explorer/Menus/CommandBar/useCommandBar.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
|
export interface CommandBarStore {
|
||||||
|
contextButtons: CommandButtonComponentProps[];
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
||||||
|
isHidden: boolean;
|
||||||
|
setIsHidden: (isHidden: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
|
contextButtons: [],
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
|
isHidden: false,
|
||||||
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
|
}));
|
||||||
159
src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx
Normal file
159
src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
makeStyles,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
MenuPopover,
|
||||||
|
MenuTrigger,
|
||||||
|
Toolbar,
|
||||||
|
ToolbarButton,
|
||||||
|
ToolbarDivider,
|
||||||
|
ToolbarGroup,
|
||||||
|
Tooltip,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import {
|
||||||
|
createPlatformButtons,
|
||||||
|
createStaticCommandBarButtons,
|
||||||
|
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||||
|
import { createKeyboardHandlers } from "Explorer/Menus/CommandBar/CommandBarUtil";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
|
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import React, { MouseEventHandler } from "react";
|
||||||
|
|
||||||
|
const useToolbarStyles = makeStyles({
|
||||||
|
toolbar: {
|
||||||
|
height: tokens.layoutRowHeight,
|
||||||
|
justifyContent: "space-between", // Ensures that the two toolbar groups are at opposite ends of the toolbar
|
||||||
|
...cosmosShorthands.borderBottom(),
|
||||||
|
},
|
||||||
|
toolbarGroup: {
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface CommandBarV2Props {
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandBarV2: React.FC<CommandBarV2Props> = ({ explorer }: CommandBarV2Props) => {
|
||||||
|
const styles = useToolbarStyles();
|
||||||
|
const selectedNodeState = useSelectedNode();
|
||||||
|
const contextButtons = useCommandBar((state) => state.contextButtons);
|
||||||
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
|
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
||||||
|
const platformButtons = createPlatformButtons();
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
setKeyboardHandlers({});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
||||||
|
const keyboardHandlers = createKeyboardHandlers(allButtons, explorer);
|
||||||
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CosmosFluentProvider>
|
||||||
|
<Toolbar className={styles.toolbar}>
|
||||||
|
<ToolbarGroup role="presentation" className={styles.toolbarGroup}>
|
||||||
|
{staticButtons.map((button, index) =>
|
||||||
|
renderButton(explorer, button, `static-${index}`, contextButtons?.length > 0),
|
||||||
|
)}
|
||||||
|
{staticButtons.length > 0 && contextButtons?.length > 0 && <ToolbarDivider />}
|
||||||
|
{contextButtons.map((button, index) => renderButton(explorer, button, `context-${index}`, false))}
|
||||||
|
</ToolbarGroup>
|
||||||
|
<ToolbarGroup role="presentation">
|
||||||
|
{platformButtons.map((button, index) => renderButton(explorer, button, `platform-${index}`, true))}
|
||||||
|
</ToolbarGroup>
|
||||||
|
</Toolbar>
|
||||||
|
</CosmosFluentProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// This allows us to migrate individual buttons over to using a JSX.Element for the icon, without requiring us to change them all at once.
|
||||||
|
function renderIcon(iconSrcOrElement: string | JSX.Element, alt?: string): JSX.Element {
|
||||||
|
if (typeof iconSrcOrElement === "string") {
|
||||||
|
return <img src={iconSrcOrElement} alt={alt} />;
|
||||||
|
}
|
||||||
|
return iconSrcOrElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderButton(
|
||||||
|
explorer: Explorer,
|
||||||
|
btn: CommandButtonComponentProps,
|
||||||
|
key: string,
|
||||||
|
iconOnly: boolean,
|
||||||
|
): JSX.Element {
|
||||||
|
if (btn.isDivider) {
|
||||||
|
return <ToolbarDivider key={key} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChildren = !!btn.children && btn.children.length > 0;
|
||||||
|
const label = btn.commandButtonLabel || btn.tooltipText;
|
||||||
|
const tooltip = btn.tooltipText || (iconOnly ? label : undefined);
|
||||||
|
const onClick: MouseEventHandler | undefined =
|
||||||
|
btn.onCommandClick && !hasChildren ? (e) => btn.onCommandClick(e, explorer) : undefined;
|
||||||
|
|
||||||
|
// We don't know which element will be the top-level element, so just slap a key on all of 'em
|
||||||
|
|
||||||
|
let button = hasChildren ? (
|
||||||
|
<MenuButton key={key} appearance="subtle" aria-label={btn.ariaLabel} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
||||||
|
{!iconOnly && label}
|
||||||
|
</MenuButton>
|
||||||
|
) : (
|
||||||
|
<ToolbarButton key={key} aria-label={btn.ariaLabel} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
||||||
|
{!iconOnly && label}
|
||||||
|
</ToolbarButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
button = (
|
||||||
|
<Tooltip key={key} content={tooltip} relationship="description" withArrow>
|
||||||
|
{button}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
button = (
|
||||||
|
<Menu key={key}>
|
||||||
|
<MenuTrigger disableButtonEnhancement>{button}</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMenuItem(explorer: Explorer, btn: CommandButtonComponentProps, key: string): JSX.Element {
|
||||||
|
const hasChildren = !!btn.children && btn.children.length > 0;
|
||||||
|
const onClick: MouseEventHandler | undefined = btn.onCommandClick
|
||||||
|
? (e) => btn.onCommandClick(e, explorer)
|
||||||
|
: undefined;
|
||||||
|
const item = (
|
||||||
|
<MenuItem key={key} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
||||||
|
{btn.commandButtonLabel || btn.tooltipText}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuTrigger disableButtonEnhancement>{item}</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||||
@@ -57,19 +56,6 @@ function openCollectionTab(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
configContext.platform === Platform.Fabric &&
|
|
||||||
!(
|
|
||||||
// whitelist the tab kinds that are allowed to be opened in Fabric
|
|
||||||
(
|
|
||||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
|
||||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//expand database first if not expanded to load the collections
|
//expand database first if not expanded to load the collections
|
||||||
if (!database.isDatabaseExpanded?.()) {
|
if (!database.isDatabaseExpanded?.()) {
|
||||||
database.expandDatabase?.();
|
database.expandDatabase?.();
|
||||||
@@ -135,28 +121,10 @@ function openCollectionTab(
|
|||||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||||
) {
|
) {
|
||||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
|
||||||
collection.onNewQueryClick(
|
collection.onNewQueryClick(
|
||||||
collection,
|
collection,
|
||||||
undefined,
|
undefined,
|
||||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||||
openQueryTabAction.splitterDirection,
|
|
||||||
openQueryTabAction.queryViewSizePercent,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
action.tabKind === ActionContracts.TabKind.MongoQuery ||
|
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
|
|
||||||
) {
|
|
||||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
|
||||||
collection.onNewMongoQueryClick(
|
|
||||||
collection,
|
|
||||||
undefined,
|
|
||||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
|
||||||
openQueryTabAction.splitterDirection,
|
|
||||||
openQueryTabAction.queryViewSizePercent,
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,11 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import {
|
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||||
FullTextPoliciesComponent,
|
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||||
getFullTextLanguageOptions,
|
|
||||||
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -34,12 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import {
|
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
isCapabilityEnabled,
|
|
||||||
isFullTextSearchEnabled,
|
|
||||||
isServerlessAccount,
|
|
||||||
isVectorSearchEnabled,
|
|
||||||
} from "Utils/CapabilityUtils";
|
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||||
@@ -118,9 +109,6 @@ export interface AddCollectionPanelState {
|
|||||||
vectorIndexingPolicy: DataModels.VectorIndex[];
|
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||||
vectorPolicyValidated: boolean;
|
vectorPolicyValidated: boolean;
|
||||||
fullTextPolicy: DataModels.FullTextPolicy;
|
|
||||||
fullTextIndexes: DataModels.FullTextIndex[];
|
|
||||||
fullTextPolicyValidated: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@@ -137,7 +125,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
createNewDatabase:
|
createNewDatabase:
|
||||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||||
isSharedThroughputChecked: getNewDatabaseSharedThroughputDefault(),
|
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||||
selectedDatabaseId:
|
selectedDatabaseId:
|
||||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||||
@@ -159,9 +147,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
vectorPolicyValidated: true,
|
||||||
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
|
|
||||||
fullTextIndexes: [],
|
|
||||||
fullTextPolicyValidated: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,9 +804,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{this.shouldShowAnalyticalStoreOptions() && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text className="panelTextBold" variant="small">
|
<Stack horizontal>
|
||||||
{this.getAnalyticalStorageContent()}
|
<Text className="panelTextBold" variant="small">
|
||||||
</Text>
|
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>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<div role="radiogroup">
|
<div role="radiogroup">
|
||||||
@@ -865,7 +863,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<Link
|
<Link
|
||||||
href="https://aka.ms/cosmosdb-synapselink"
|
href="https://aka.ms/cosmosdb-synapselink"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
|
||||||
className="capacitycalculator-link"
|
className="capacitycalculator-link"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
@@ -893,9 +890,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
<VectorEmbeddingPoliciesComponent
|
<AddVectorEmbeddingPolicyForm
|
||||||
vectorEmbeddings={this.state.vectorEmbeddingPolicy}
|
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
||||||
vectorIndexes={this.state.vectorIndexingPolicy}
|
vectorIndex={this.state.vectorIndexingPolicy}
|
||||||
onVectorEmbeddingChange={(
|
onVectorEmbeddingChange={(
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||||
vectorIndexingPolicy: DataModels.VectorIndex[],
|
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||||
@@ -909,34 +906,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{this.shouldShowFullTextSearchParameters() && (
|
|
||||||
<Stack>
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Container Full Text Search Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
this.scrollToSection("collapsibleFullTextPolicySectionContent");
|
|
||||||
}}
|
|
||||||
//TODO: uncomment when learn more text becomes available
|
|
||||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
|
||||||
>
|
|
||||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
|
||||||
<FullTextPoliciesComponent
|
|
||||||
fullTextPolicy={this.state.fullTextPolicy}
|
|
||||||
onFullTextPathChange={(
|
|
||||||
fullTextPolicy: DataModels.FullTextPolicy,
|
|
||||||
fullTextIndexes: DataModels.FullTextIndex[],
|
|
||||||
fullTextPolicyValidated: boolean,
|
|
||||||
) => {
|
|
||||||
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{userContext.apiType !== "Tables" && (
|
{userContext.apiType !== "Tables" && (
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
@@ -1169,6 +1138,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSharedThroughputDefault(): boolean {
|
||||||
|
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
|
||||||
|
}
|
||||||
|
|
||||||
private getFreeTierIndexingText(): string {
|
private getFreeTierIndexingText(): string {
|
||||||
return this.state.enableIndexing
|
return this.state.enableIndexing
|
||||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||||
@@ -1218,16 +1191,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageContent(): JSX.Element {
|
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||||
impacting the performance of transactional workloads.{" "}
|
impacting the performance of transactional workloads.{" "}
|
||||||
<Link
|
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
|
||||||
target="_blank"
|
|
||||||
href="https://aka.ms/analytical-store-overview"
|
|
||||||
>
|
|
||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1246,19 +1215,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: uncomment when learn more text becomes available
|
|
||||||
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
|
||||||
// return (
|
|
||||||
// <Text variant="small">
|
|
||||||
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
|
||||||
// magna aliqua.{" "}
|
|
||||||
// <Link target="_blank" href="https://aka.ms/CosmosFullTextSearch">
|
|
||||||
// Learn more
|
|
||||||
// </Link>
|
|
||||||
// </Text>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1322,10 +1278,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowFullTextSearchParameters() {
|
|
||||||
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||||
if (this.state.uniqueKeys?.length === 0) {
|
if (this.state.uniqueKeys?.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -1382,16 +1334,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters()) {
|
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
||||||
if (!this.state.vectorPolicyValidated) {
|
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.fullTextPolicyValidated) {
|
|
||||||
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1482,10 +1427,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowFullTextSearchParameters()) {
|
|
||||||
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
database: {
|
database: {
|
||||||
id: databaseId,
|
id: databaseId,
|
||||||
@@ -1545,7 +1486,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
fullTextPolicy: this.state.fullTextPolicy,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({ isExecuting: true });
|
this.setState({ isExecuting: true });
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
@@ -49,7 +48,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
|
|
||||||
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
||||||
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
||||||
getNewDatabaseSharedThroughputDefault(),
|
subscriptionType !== SubscriptionType.EA && !isServerlessAccount(),
|
||||||
);
|
);
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
const [formErrors, setFormErrors] = useState<string>("");
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={true}
|
||||||
label="Provision throughput"
|
label="Provision throughput"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
@@ -90,6 +90,14 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<ThroughputInput
|
||||||
|
isDatabase={true}
|
||||||
|
isSharded={true}
|
||||||
|
onCostAcknowledgeChange={[Function]}
|
||||||
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
|
setThroughputValue={[Function]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
message:
|
message:
|
||||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
};
|
};
|
||||||
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`;
|
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
|
||||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
@@ -132,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
<div className="confirmDeleteInput">
|
<div className="confirmDeleteInput">
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text variant="small">{confirmDatabase}</Text>
|
<Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
|
||||||
<TextField
|
<TextField
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import {
|
|
||||||
AuthError as msalAuthError,
|
|
||||||
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
|
|
||||||
} from "@azure/msal-browser";
|
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ChoiceGroup,
|
ChoiceGroup,
|
||||||
@@ -9,6 +5,8 @@ import {
|
|||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
ISpinButtonStyles,
|
ISpinButtonStyles,
|
||||||
IToggleStyles,
|
IToggleStyles,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
Position,
|
Position,
|
||||||
SpinButton,
|
SpinButton,
|
||||||
Toggle,
|
Toggle,
|
||||||
@@ -32,7 +30,6 @@ import {
|
|||||||
} from "Shared/StorageUtility";
|
} from "Shared/StorageUtility";
|
||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
@@ -111,6 +108,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
||||||
: Constants.RBACOptions.setAutomaticRBACOption,
|
: Constants.RBACOptions.setAutomaticRBACOption,
|
||||||
);
|
);
|
||||||
|
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
|
||||||
|
|
||||||
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||||
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||||
@@ -174,26 +172,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
const isEmulator = configContext.platform === Platform.Emulator;
|
|
||||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||||
const showRetrySettings =
|
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||||
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") &&
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||||
!isEmulator;
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||||
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 =
|
const shouldShowCopilotSampleDBOption =
|
||||||
userContext.apiType === "SQL" &&
|
userContext.apiType === "SQL" &&
|
||||||
useQueryCopilot.getState().copilotEnabled &&
|
useQueryCopilot.getState().copilotEnabled &&
|
||||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
useDatabases.getState().sampleDataResourceTokenCollection;
|
||||||
!isEmulator;
|
|
||||||
|
|
||||||
const handlerOnSubmit = async () => {
|
const handlerOnSubmit = async () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
@@ -204,17 +191,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
|
|
||||||
if (
|
|
||||||
enableDataPlaneRBACOption !== LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ||
|
|
||||||
retryAttempts !== LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ||
|
|
||||||
retryInterval !== LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval) ||
|
|
||||||
MaxWaitTimeInSeconds !== LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
|
|
||||||
) {
|
|
||||||
updateUserContext({
|
|
||||||
refreshCosmosClient: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||||
if (
|
if (
|
||||||
@@ -224,29 +200,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
) {
|
) {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: true,
|
dataPlaneRbacEnabled: true,
|
||||||
|
hasDataPlaneRbacSettingChanged: true,
|
||||||
});
|
});
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
try {
|
|
||||||
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
|
|
||||||
updateUserContext({ aadToken: aadToken });
|
|
||||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
|
||||||
} catch (authError) {
|
|
||||||
if (
|
|
||||||
authError instanceof msalAuthError &&
|
|
||||||
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
|
|
||||||
) {
|
|
||||||
logConsoleError(
|
|
||||||
`We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logConsoleError(
|
|
||||||
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: false,
|
dataPlaneRbacEnabled: false,
|
||||||
|
hasDataPlaneRbacSettingChanged: true,
|
||||||
});
|
});
|
||||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
||||||
@@ -387,6 +347,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
option: IChoiceGroupOption,
|
option: IChoiceGroupOption,
|
||||||
): void => {
|
): void => {
|
||||||
setEnableDataPlaneRBACOption(option.key);
|
setEnableDataPlaneRBACOption(option.key);
|
||||||
|
|
||||||
|
const shouldShowWarning =
|
||||||
|
(option.key === Constants.RBACOptions.setTrueRBACOption ||
|
||||||
|
(option.key === Constants.RBACOptions.setAutomaticRBACOption &&
|
||||||
|
userContext.databaseAccount.properties.disableLocalAuth === true)) &&
|
||||||
|
!useDataPlaneRbac.getState().aadTokenUpdated;
|
||||||
|
setShowDataPlaneRBACWarning(shouldShowWarning);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
@@ -502,7 +469,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className={`paneMainContent ${styles.container}`}>
|
<div className={`paneMainContent ${styles.container}`}>
|
||||||
<Accordion className={`customAccordion ${styles.firstItem}`}>
|
<Accordion className={styles.firstItem}>
|
||||||
{shouldShowQueryPageOptions && (
|
{shouldShowQueryPageOptions && (
|
||||||
<AccordionItem value="1">
|
<AccordionItem value="1">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -552,37 +519,51 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{showEnableEntraIdRbac && (
|
{userContext.apiType === "SQL" &&
|
||||||
<AccordionItem value="2">
|
userContext.authType === AuthType.AAD &&
|
||||||
<AccordionHeader>
|
configContext.platform !== Platform.Fabric && (
|
||||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
<AccordionItem value="2">
|
||||||
</AccordionHeader>
|
<AccordionHeader>
|
||||||
<AccordionPanel>
|
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||||
<div className={styles.settingsSectionContainer}>
|
</AccordionHeader>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<AccordionPanel>
|
||||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
|
<div className={styles.settingsSectionContainer}>
|
||||||
RBAC.
|
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
||||||
<a
|
<MessageBar
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
messageBarType={MessageBarType.warning}
|
||||||
target="_blank"
|
isMultiline={true}
|
||||||
rel="noopener noreferrer"
|
onDismiss={() => setShowDataPlaneRBACWarning(false)}
|
||||||
>
|
dismissButtonAriaLabel="Close"
|
||||||
{" "}
|
>
|
||||||
Learn more{" "}
|
Please click on "Login for Entra ID RBAC" button prior to performing Entra ID RBAC
|
||||||
</a>
|
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}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChoiceGroup
|
</AccordionPanel>
|
||||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
</AccordionItem>
|
||||||
options={dataPlaneRBACOptionsList}
|
)}
|
||||||
styles={choiceButtonStyles}
|
|
||||||
selectedKey={enableDataPlaneRBACOption}
|
{userContext.apiType === "SQL" && (
|
||||||
onChange={handleOnDataPlaneRBACOptionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
)}
|
|
||||||
{userContext.apiType === "SQL" && !isEmulator && (
|
|
||||||
<>
|
<>
|
||||||
<AccordionItem value="3">
|
<AccordionItem value="3">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -680,103 +661,102 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{showRetrySettings && (
|
|
||||||
<AccordionItem value="6">
|
<AccordionItem value="6">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Retry Settings</div>
|
<div className={styles.header}>Retry Settings</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className={styles.settingsSectionDescription}>
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
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>
|
||||||
</AccordionPanel>
|
<div>
|
||||||
</AccordionItem>
|
<span className={styles.subHeader}>Max retry attempts</span>
|
||||||
)}
|
<InfoTooltip className={styles.headerIcon}>
|
||||||
{!isEmulator && (
|
Max number of retries to be performed for a request. Default value 9.
|
||||||
<AccordionItem value="7">
|
</InfoTooltip>
|
||||||
<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>
|
</div>
|
||||||
</AccordionPanel>
|
<SpinButton
|
||||||
</AccordionItem>
|
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>
|
||||||
|
|
||||||
{shouldShowCrossPartitionOption && (
|
{shouldShowCrossPartitionOption && (
|
||||||
<AccordionItem value="8">
|
<AccordionItem value="8">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -802,6 +782,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowParallelismOption && (
|
{shouldShowParallelismOption && (
|
||||||
<AccordionItem value="9">
|
<AccordionItem value="9">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -835,6 +816,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowPriorityLevelOption && (
|
{shouldShowPriorityLevelOption && (
|
||||||
<AccordionItem value="10">
|
<AccordionItem value="10">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -858,6 +840,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowGraphAutoVizOption && (
|
{shouldShowGraphAutoVizOption && (
|
||||||
<AccordionItem value="11">
|
<AccordionItem value="11">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -879,6 +862,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowCopilotSampleDBOption && (
|
{shouldShowCopilotSampleDBOption && (
|
||||||
<AccordionItem value="12">
|
<AccordionItem value="12">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||||
>
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
className="___1uf6361_0000000 fz7g6wx"
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="1"
|
value="1"
|
||||||
@@ -572,7 +572,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||||
>
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
className="___1uf6361_0000000 fz7g6wx"
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="6"
|
value="6"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
|
|||||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
|
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
||||||
|
|
||||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||||
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
component = render(
|
component = render(
|
||||||
<VectorEmbeddingPoliciesComponent
|
<AddVectorEmbeddingPolicyForm
|
||||||
vectorEmbeddings={mockVectorEmbedding}
|
vectorEmbedding={mockVectorEmbedding}
|
||||||
vectorIndexes={mockVectorIndex}
|
vectorIndex={mockVectorIndex}
|
||||||
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("calls onDelete when delete button is clicked", async () => {
|
test("calls onDelete when delete button is clicked", async () => {
|
||||||
const deleteButton = component.container.querySelector("#delete-Vector-embedding-1");
|
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
||||||
fireEvent.click(deleteButton);
|
fireEvent.click(deleteButton);
|
||||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||||
@@ -49,19 +49,21 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
|
|
||||||
test("validates input correctly", async () => {
|
test("validates input correctly", async () => {
|
||||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||||
await waitFor(() => expect(screen.getByText("Path should not be empty")).toBeInTheDocument(), {
|
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() =>
|
() =>
|
||||||
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(),
|
expect(
|
||||||
|
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
||||||
|
).toBeInTheDocument(),
|
||||||
{
|
{
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||||
await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), {
|
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
await waitFor(
|
await waitFor(
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
IStyleFunctionOrObject,
|
||||||
|
ITextFieldStyleProps,
|
||||||
|
ITextFieldStyles,
|
||||||
|
IconButton,
|
||||||
|
Label,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
getDistanceFunctionOptions,
|
||||||
|
getIndexTypeOptions,
|
||||||
|
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
|
||||||
|
export interface IAddVectorEmbeddingPolicyFormProps {
|
||||||
|
vectorEmbedding: VectorEmbedding[];
|
||||||
|
vectorIndex: VectorIndex[];
|
||||||
|
onVectorEmbeddingChange: (
|
||||||
|
vectorEmbeddings: VectorEmbedding[],
|
||||||
|
vectorIndexingPolicies: VectorIndex[],
|
||||||
|
validationPassed: boolean,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorEmbeddingPolicyData {
|
||||||
|
path: string;
|
||||||
|
dataType: VectorEmbedding["dataType"];
|
||||||
|
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||||
|
dimensions: number;
|
||||||
|
indexType: VectorIndex["type"] | "none";
|
||||||
|
pathError: string;
|
||||||
|
dimensionsError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||||
|
|
||||||
|
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||||
|
fieldGroup: {
|
||||||
|
height: 27,
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "0 8px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownStyles = {
|
||||||
|
title: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
|
||||||
|
vectorEmbedding,
|
||||||
|
vectorIndex,
|
||||||
|
onVectorEmbeddingChange,
|
||||||
|
}): JSX.Element => {
|
||||||
|
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (!path) {
|
||||||
|
error = "Vector embedding path should not be empty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
index >= 0 &&
|
||||||
|
vectorEmbeddingPolicyData?.find(
|
||||||
|
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||||
|
dataIndex !== index && vectorEmbedding.path === path,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
error = "Vector embedding path is already defined";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||||
|
let error = "";
|
||||||
|
if (dimension <= 0 || dimension > 4096) {
|
||||||
|
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
|
||||||
|
}
|
||||||
|
if (indexType === "flat" && dimension > 505) {
|
||||||
|
error = "Maximum allowed dimension for flat index is 505";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
|
||||||
|
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||||
|
vectorEmbedding.forEach((embedding) => {
|
||||||
|
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
|
||||||
|
mergedData.push({
|
||||||
|
...embedding,
|
||||||
|
indexType: matchingIndex?.type || "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||||
|
initializeData(vectorEmbedding, vectorIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
propagateData();
|
||||||
|
}, [vectorEmbeddingPolicyData]);
|
||||||
|
|
||||||
|
const propagateData = () => {
|
||||||
|
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||||
|
dataType: policy.dataType,
|
||||||
|
dimensions: policy.dimensions,
|
||||||
|
distanceFunction: policy.distanceFunction,
|
||||||
|
path: policy.path,
|
||||||
|
}));
|
||||||
|
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
|
||||||
|
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||||
|
.map(
|
||||||
|
(policy) =>
|
||||||
|
({
|
||||||
|
path: policy.path,
|
||||||
|
type: policy.indexType,
|
||||||
|
}) as VectorIndex,
|
||||||
|
);
|
||||||
|
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||||
|
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||||
|
);
|
||||||
|
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value.trim();
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||||
|
vectorEmbeddings[index].path = "/" + value;
|
||||||
|
} else {
|
||||||
|
vectorEmbeddings[index].path = value;
|
||||||
|
}
|
||||||
|
const error = onVectorEmbeddingPathError(value, index);
|
||||||
|
vectorEmbeddings[index].pathError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value.trim()) || 0;
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].dimensions = value;
|
||||||
|
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].indexType = option.key as never;
|
||||||
|
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPolicyChange = (
|
||||||
|
index: number,
|
||||||
|
option: IDropdownOption,
|
||||||
|
property: VectorEmbeddingPolicyProperty,
|
||||||
|
): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
vectorEmbeddings[index][property] = option.key as never;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
setVectorEmbeddingPolicyData([
|
||||||
|
...vectorEmbeddingPolicyData,
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "euclidean",
|
||||||
|
dimensions: 0,
|
||||||
|
indexType: "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(""),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (index: number) => {
|
||||||
|
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
{vectorEmbeddingPolicyData.length > 0 &&
|
||||||
|
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||||
|
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
margin: "0 0 6px 20px !important",
|
||||||
|
paddingLeft: 20,
|
||||||
|
width: "80%",
|
||||||
|
borderLeft: "1px solid",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
|
||||||
|
<TextField
|
||||||
|
id={`vector-policy-path-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
placeholder="/vector1"
|
||||||
|
styles={textFieldStyles}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||||
|
value={vectorEmbeddingPolicy.path || ""}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDataTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDistanceFunctionOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
|
||||||
|
<TextField
|
||||||
|
id={`vector-policy-dimension-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onVectorEmbeddingDimensionsChange(index, event)
|
||||||
|
}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getIndexTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingIndexTypeChange(index, option)
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
id={`delete-vector-policy-${index + 1}`}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27, margin: "auto" }}
|
||||||
|
onClick={() => onDelete(index)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
))}
|
||||||
|
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||||
|
Add vector embedding
|
||||||
|
</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -106,7 +106,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={true}
|
||||||
label="Share throughput across containers"
|
label="Share throughput across containers"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
@@ -137,6 +137,14 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<ThroughputInput
|
||||||
|
isDatabase={true}
|
||||||
|
isSharded={true}
|
||||||
|
onCostAcknowledgeChange={[Function]}
|
||||||
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
|
setThroughputValue={[Function]}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Separator
|
<Separator
|
||||||
className="panelSeparator"
|
className="panelSeparator"
|
||||||
@@ -255,14 +263,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</CustomizedDefaultButton>
|
</CustomizedDefaultButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ThroughputInput
|
|
||||||
isDatabase={false}
|
|
||||||
isSharded={true}
|
|
||||||
onCostAcknowledgeChange={[Function]}
|
|
||||||
setIsAutoscale={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
setThroughputValue={[Function]}
|
|
||||||
/>
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
@@ -309,24 +309,40 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
<Stack
|
<Stack
|
||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
>
|
>
|
||||||
<Text
|
<Stack
|
||||||
className="panelTextBold"
|
horizontal={true}
|
||||||
variant="small"
|
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
className="panelTextBold"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
Analytical store
|
||||||
|
|
||||||
<StyledLinkBase
|
|
||||||
aria-label="Learn more about analytical store."
|
|
||||||
href="https://aka.ms/analytical-store-overview"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</StyledLinkBase>
|
|
||||||
</Text>
|
</Text>
|
||||||
</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>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
@@ -384,7 +400,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
. Enable Synapse Link for this Cosmos DB account.
|
. Enable Synapse Link for this Cosmos DB account.
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
aria-label="Learn more about Azure Synapse Link."
|
|
||||||
className="capacitycalculator-link"
|
className="capacitycalculator-link"
|
||||||
href="https://aka.ms/cosmosdb-synapselink"
|
href="https://aka.ms/cosmosdb-synapselink"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -361,11 +361,13 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="css-113"
|
className="css-113"
|
||||||
>
|
>
|
||||||
Confirm by typing the Database id (name)
|
Confirm by typing the
|
||||||
|
Database
|
||||||
|
id
|
||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Confirm by typing the Database id (name)"
|
ariaLabel="Confirm by typing the Database id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
@@ -380,7 +382,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
ariaLabel="Confirm by typing the Database id (name)"
|
ariaLabel="Confirm by typing the Database id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
@@ -675,7 +677,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
aria-label="Confirm by typing the Database id (name)"
|
aria-label="Confirm by typing the Database id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="ms-TextField-field field-117"
|
className="ms-TextField-field field-117"
|
||||||
data-test="Input:confirmDatabaseId"
|
data-test="Input:confirmDatabaseId"
|
||||||
|
|||||||
@@ -79,13 +79,9 @@ export const QueryCopilotFeedbackModal = ({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
||||||
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://go.microsoft.com/fwlink/?LinkId=521839" target="_blank">
|
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||||
Privacy statement
|
Privacy statement
|
||||||
</Link>
|
</Link>
|
||||||
}{" "}
|
}{" "}
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -236,10 +236,10 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -373,10 +373,10 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -510,10 +510,10 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -647,10 +647,10 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -784,10 +784,10 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
@@ -936,10 +936,10 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
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
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
href="https://privacy.microsoft.com/privacystatement"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Privacy statement
|
Privacy statement
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const itemRefs = useRef([]);
|
const itemRefs = useRef([]);
|
||||||
const searchInputRef = useRef(null);
|
const searchInputRef = useRef(null);
|
||||||
const copyQueryRef = useRef(null);
|
|
||||||
const {
|
const {
|
||||||
openFeedbackModal,
|
openFeedbackModal,
|
||||||
hideFeedbackModalForLikedQueries,
|
hideFeedbackModalForLikedQueries,
|
||||||
@@ -133,7 +132,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
document.body.removeChild(queryElement);
|
document.body.removeChild(queryElement);
|
||||||
|
|
||||||
setshowCopyPopup(true);
|
setshowCopyPopup(true);
|
||||||
copyQueryRef.current.focus();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setshowCopyPopup(false);
|
setshowCopyPopup(false);
|
||||||
}, 6000);
|
}, 6000);
|
||||||
@@ -307,7 +305,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
if (isGeneratingQuery === null) {
|
if (isGeneratingQuery === null) {
|
||||||
return " ";
|
return " ";
|
||||||
} else if (isGeneratingQuery) {
|
} else if (isGeneratingQuery) {
|
||||||
return "Thinking";
|
return "Content is loading";
|
||||||
} else {
|
} else {
|
||||||
return "Content is updated";
|
return "Content is updated";
|
||||||
}
|
}
|
||||||
@@ -402,7 +400,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: "Send" }}
|
iconProps={{ iconName: "Send" }}
|
||||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||||
allowDisabledFocus={true}
|
|
||||||
style={{ background: "none" }}
|
style={{ background: "none" }}
|
||||||
onClick={() => startGenerateQueryProcess()}
|
onClick={() => startGenerateQueryProcess()}
|
||||||
aria-label="Send"
|
aria-label="Send"
|
||||||
@@ -679,7 +676,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
)}
|
)}
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
className="copyQuery"
|
className="copyQuery"
|
||||||
elementRef={copyQueryRef}
|
|
||||||
onClick={copyGeneratedCode}
|
onClick={copyGeneratedCode}
|
||||||
iconProps={{ iconName: "Copy" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||||
@@ -710,9 +706,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{(showFeedbackBar || isGeneratingQuery) && (
|
|
||||||
<span role="alert" className="screenReaderOnly" aria-label={getAriaLabel()} />
|
|
||||||
)}
|
|
||||||
{isGeneratingQuery && (
|
{isGeneratingQuery && (
|
||||||
<ProgressIndicator
|
<ProgressIndicator
|
||||||
label="Thinking..."
|
label="Thinking..."
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Stack } from "@fluentui/react";
|
|||||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
|||||||
import { Allotment, AllotmentHandle } from "allotment";
|
import { Allotment, AllotmentHandle } from "allotment";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -109,7 +109,6 @@ interface GlobalCommand {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
keyboardAction?: KeyboardAction;
|
keyboardAction?: KeyboardAction;
|
||||||
ref?: React.RefObject<HTMLButtonElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||||
@@ -119,7 +118,6 @@ 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.
|
// 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.
|
// 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 [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||||
const primaryFocusableRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
@@ -179,16 +177,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
}, [actions, setKeyboardActions]);
|
}, [actions, setKeyboardActions]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (primaryFocusableRef.current) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
primaryFocusableRef.current.focus();
|
|
||||||
}, 0);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!primaryAction) {
|
if (!primaryAction) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -196,7 +184,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
||||||
{actions.length === 1 ? (
|
{actions.length === 1 ? (
|
||||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}>
|
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick}>
|
||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@@ -206,7 +194,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
<div ref={setGlobalCommandButton}>
|
<div ref={setGlobalCommandButton}>
|
||||||
<SplitButton
|
<SplitButton
|
||||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||||
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }}
|
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||||
className={styles.globalCommandsSplitButton}
|
className={styles.globalCommandsSplitButton}
|
||||||
icon={primaryAction.icon}
|
icon={primaryAction.icon}
|
||||||
>
|
>
|
||||||
@@ -294,69 +282,67 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidebarContainer">
|
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
||||||
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
{/* Collections Tree - Start */}
|
||||||
{/* Collections Tree - Start */}
|
{hasSidebar && (
|
||||||
{hasSidebar && (
|
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
<div className={styles.sidebarContainer}>
|
||||||
<div className={styles.sidebarContainer}>
|
{loading && (
|
||||||
{loading && (
|
// The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here.
|
||||||
// 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
|
||||||
// https://github.com/microsoft/fluentui/issues/29076
|
<div className={styles.loadingProgressBar} title="Refreshing tree..." />
|
||||||
<div className={styles.loadingProgressBar} title="Refreshing tree..." />
|
)}
|
||||||
)}
|
{expanded ? (
|
||||||
{expanded ? (
|
<>
|
||||||
<>
|
<div className={styles.floatingControlsContainer}>
|
||||||
<div className={styles.floatingControlsContainer}>
|
<div className={styles.floatingControls}>
|
||||||
<div className={styles.floatingControls}>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
data-test="Sidebar/RefreshButton"
|
||||||
data-test="Sidebar/RefreshButton"
|
className={styles.floatingControlButton}
|
||||||
className={styles.floatingControlButton}
|
disabled={loading}
|
||||||
disabled={loading}
|
title="Refresh"
|
||||||
title="Refresh"
|
onClick={onRefreshClick}
|
||||||
onClick={onRefreshClick}
|
>
|
||||||
>
|
<ArrowSync12Regular />
|
||||||
<ArrowSync12Regular />
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={styles.floatingControlButton}
|
||||||
className={styles.floatingControlButton}
|
title="Collapse sidebar"
|
||||||
title="Collapse sidebar"
|
onClick={() => collapse()}
|
||||||
onClick={() => collapse()}
|
>
|
||||||
>
|
<ChevronLeft12Regular />
|
||||||
<ChevronLeft12Regular />
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
className={styles.expandedContent}
|
<div
|
||||||
style={!hasGlobalCommands ? { gridTemplateRows: "1fr" } : undefined}
|
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()}
|
|
||||||
>
|
>
|
||||||
<ChevronRight12Regular />
|
{hasGlobalCommands && <GlobalCommands explorer={explorer} />}
|
||||||
</button>
|
<ResourceTree explorer={explorer} />
|
||||||
)}
|
</div>
|
||||||
</div>
|
</>
|
||||||
</CosmosFluentProvider>
|
) : (
|
||||||
</Allotment.Pane>
|
<button
|
||||||
)}
|
type="button"
|
||||||
<Allotment.Pane minSize={200}>
|
className={styles.floatingControlButton}
|
||||||
<Tabs explorer={explorer} />
|
title="Expand sidebar"
|
||||||
|
onClick={() => expand()}
|
||||||
|
>
|
||||||
|
<ChevronRight12Regular />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CosmosFluentProvider>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
</Allotment>
|
)}
|
||||||
</div>
|
<Allotment.Pane minSize={200}>
|
||||||
|
<Tabs explorer={explorer} />
|
||||||
|
</Allotment.Pane>
|
||||||
|
</Allotment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
<img src={imgSrc} />
|
||||||
</div>
|
</div>
|
||||||
<Stack style={{ marginLeft: 16 }}>
|
<Stack style={{ marginLeft: 16 }}>
|
||||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||||
|
|||||||
@@ -757,10 +757,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
CassandraProxyEndpoints.Mooncake,
|
CassandraProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
|
|||||||
@@ -3,11 +3,17 @@
|
|||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import {
|
import {
|
||||||
AppStateComponentNames,
|
AppStateComponentNames,
|
||||||
deleteSubComponentState,
|
deleteState,
|
||||||
readSubComponentState,
|
loadState,
|
||||||
saveSubComponentState,
|
saveState,
|
||||||
|
saveStateDebounced,
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
|
const componentName = AppStateComponentNames.DocumentsTab;
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
@@ -15,7 +21,6 @@ export enum SubComponentName {
|
|||||||
MainTabDivider = "MainTabDivider",
|
MainTabDivider = "MainTabDivider",
|
||||||
ColumnsSelection = "ColumnsSelection",
|
ColumnsSelection = "ColumnsSelection",
|
||||||
ColumnSort = "ColumnSort",
|
ColumnSort = "ColumnSort",
|
||||||
CurrentFilter = "CurrentFilter",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||||
@@ -25,22 +30,84 @@ export type TabDivider = { leftPaneWidthPercent: number };
|
|||||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||||
|
|
||||||
// Wrap the ...SubComponentState functions for type safety
|
/**
|
||||||
|
*
|
||||||
export const readDocumentsTabSubComponentState = <T>(
|
* @param subComponentName
|
||||||
|
* @param collection
|
||||||
|
* @param defaultValue Will be returned if persisted state is not found
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const readSubComponentState = <T>(
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
|
): 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;
|
||||||
|
}
|
||||||
|
|
||||||
export const saveDocumentsTabSubComponentState = <T>(
|
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>(
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
state: T,
|
state: T,
|
||||||
debounce?: boolean,
|
debounce?: boolean,
|
||||||
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
|
): 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;
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteDocumentsTabSubComponentState = (
|
(debounce ? saveStateDebounced : saveState)(
|
||||||
subComponentName: SubComponentName,
|
{
|
||||||
collection: ViewModels.CollectionBase,
|
componentName: componentName,
|
||||||
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);
|
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(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Platform, updateConfigContext } from "ConfigContext";
|
|||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import {
|
import {
|
||||||
ButtonsDependencies,
|
ButtonsDependencies,
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
@@ -49,7 +49,6 @@ jest.mock("Common/dataAccess/queryDocuments", () => ({
|
|||||||
requestCharge: 1,
|
requestCharge: 1,
|
||||||
activityId: "activityId",
|
activityId: "activityId",
|
||||||
indexMetrics: "indexMetrics",
|
indexMetrics: "indexMetrics",
|
||||||
correlatedActivityId: undefined,
|
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
@@ -386,6 +385,22 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
it("should render the page", () => {
|
it("should render the page", () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clicking on Edit filter should render the Apply Filter button", () => {
|
||||||
|
wrapper
|
||||||
|
.findWhere((node) => node.text() === "Edit Filter")
|
||||||
|
.at(0)
|
||||||
|
.simulate("click");
|
||||||
|
expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on Edit filter should render input for filter", () => {
|
||||||
|
wrapper
|
||||||
|
.findWhere((node) => node.text() === "Edit Filter")
|
||||||
|
.at(0)
|
||||||
|
.simulate("click");
|
||||||
|
expect(wrapper.find("Input.filterInput").exists()).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Command bar buttons", () => {
|
describe("Command bar buttons", () => {
|
||||||
@@ -446,7 +461,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -456,7 +471,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
@@ -468,7 +483,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
await useCommandBar
|
await useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled();
|
expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled();
|
||||||
@@ -479,7 +494,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ProgressModalDialog).toHaveBeenCalled();
|
expect(ProgressModalDialog).toHaveBeenCalled();
|
||||||
@@ -493,7 +508,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The implementation uses setTimeout, so wait for it to finish
|
// The implementation uses setTimeout, so wait for it to finish
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Input,
|
||||||
Link,
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarBody,
|
MessageBarBody,
|
||||||
@@ -9,7 +10,8 @@ import {
|
|||||||
makeStyles,
|
makeStyles,
|
||||||
shorthands,
|
shorthands,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { Dismiss16Filled } from "@fluentui/react-icons";
|
||||||
|
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { createDocument } from "Common/dataAccess/createDocument";
|
import { createDocument } from "Common/dataAccess/createDocument";
|
||||||
@@ -21,23 +23,20 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
|||||||
import { readDocument } from "Common/dataAccess/readDocument";
|
import { readDocument } from "Common/dataAccess/readDocument";
|
||||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
ColumnsSelection,
|
ColumnsSelection,
|
||||||
FilterHistory,
|
FilterHistory,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
TabDivider,
|
TabDivider,
|
||||||
deleteDocumentsTabSubComponentState,
|
readSubComponentState,
|
||||||
readDocumentsTabSubComponentState,
|
saveSubComponentState,
|
||||||
saveDocumentsTabSubComponentState,
|
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
@@ -75,7 +74,6 @@ const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we
|
|||||||
const NO_SQL_THROTTLING_DOC_URL =
|
const NO_SQL_THROTTLING_DOC_URL =
|
||||||
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
||||||
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
||||||
const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer";
|
|
||||||
|
|
||||||
const loadMoreHeight = LayoutConstants.rowHeight;
|
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||||
export const useDocumentsTabStyles = makeStyles({
|
export const useDocumentsTabStyles = makeStyles({
|
||||||
@@ -92,6 +90,12 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
},
|
},
|
||||||
|
filterInput: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
appliedFilter: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
tableContainer: {
|
tableContainer: {
|
||||||
marginRight: tokens.spacingHorizontalXXXL,
|
marginRight: tokens.spacingHorizontalXXXL,
|
||||||
},
|
},
|
||||||
@@ -142,8 +146,6 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
private title: string;
|
private title: string;
|
||||||
private resourceTokenPartitionKey: string;
|
private resourceTokenPartitionKey: string;
|
||||||
|
|
||||||
protected persistedState: OpenCollectionTab;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
@@ -151,13 +153,6 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
this.title = options.title;
|
this.title = options.title;
|
||||||
this.partitionKey = options.partitionKey;
|
this.partitionKey = options.partitionKey;
|
||||||
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||||
|
|
||||||
this.persistedState = {
|
|
||||||
actionType: ActionType.OpenCollectionTab,
|
|
||||||
tabKind: options.isPreferredApiMongoDB ? TabKind.MongoDocuments : TabKind.SQLDocuments,
|
|
||||||
databaseResourceId: options.collection.databaseId,
|
|
||||||
collectionResourceId: options.collection.id(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
@@ -561,6 +556,8 @@ export interface IDocumentsTabComponentProps {
|
|||||||
isTabActive: boolean;
|
isTabActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
||||||
|
|
||||||
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
||||||
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
|
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
|
||||||
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
||||||
@@ -586,12 +583,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onIsExecutingChange,
|
onIsExecutingChange,
|
||||||
isTabActive,
|
isTabActive,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [filterContent, setFilterContent] = useState<string>(() =>
|
const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true);
|
||||||
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
|
const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false);
|
||||||
);
|
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
||||||
|
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
||||||
|
const [filterContent, setFilterContent] = useState<string>("");
|
||||||
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
const filterInput = useRef<HTMLInputElement>(null);
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
@@ -611,7 +610,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
// Table user clicked on this row
|
// Table user clicked on this row
|
||||||
const [clickedRowIndex, setClickedRowIndex] = useState<number>(RESET_INDEX);
|
const [clickedRowIndex, setClickedRowIndex] = useState<number>(RESET_INDEX);
|
||||||
// Table multiple selection
|
// Table multiple selection
|
||||||
const [selectedRows, setSelectedRows] = React.useState<Set<TableRowId>>(() => new Set<TableRowId>());
|
const [selectedRows, setSelectedRows] = React.useState<Set<TableRowId>>(() => new Set<TableRowId>([0]));
|
||||||
|
|
||||||
// Command buttons
|
// Command buttons
|
||||||
const [editorState, setEditorState] = useState<ViewModels.DocumentExplorerState>(
|
const [editorState, setEditorState] = useState<ViewModels.DocumentExplorerState>(
|
||||||
@@ -620,7 +619,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -635,7 +634,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// User's filter history
|
// User's filter history
|
||||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||||
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
);
|
);
|
||||||
|
|
||||||
// For progress bar for bulk delete (noSql)
|
// For progress bar for bulk delete (noSql)
|
||||||
@@ -658,6 +657,29 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFilterFocused) {
|
||||||
|
filterInput.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isFilterFocused]);
|
||||||
|
|
||||||
|
// 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).
|
* Recursively delete all documents by retrying throttled requests (429).
|
||||||
* This only works for NoSQL, because the bulk response includes status for each delete document request.
|
* This only works for NoSQL, because the bulk response includes status for each delete document request.
|
||||||
@@ -751,6 +773,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}, timeout);
|
}, timeout);
|
||||||
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
||||||
|
|
||||||
|
const applyFilterButton = {
|
||||||
|
enabled: true,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
const partitionKey: DataModels.PartitionKey = useMemo(
|
const partitionKey: DataModels.PartitionKey = useMemo(
|
||||||
() => _partitionKey || (_collection && _collection.partitionKey),
|
() => _partitionKey || (_collection && _collection.partitionKey),
|
||||||
[_collection, _partitionKey],
|
[_collection, _partitionKey],
|
||||||
@@ -777,7 +804,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -821,8 +848,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
// This is executed in onActivate() in the original code.
|
// This is executed in onActivate() in the original code.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKeyboardActions({
|
setKeyboardActions({
|
||||||
|
[KeyboardAction.SEARCH]: () => {
|
||||||
|
onShowFilterClick();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||||
updateFilterContent("");
|
setFilterContent("");
|
||||||
refreshDocumentsGrid(true);
|
refreshDocumentsGrid(true);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -1303,6 +1334,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onShowFilterClick = () => {
|
||||||
|
setIsFilterCreated(true);
|
||||||
|
setIsFilterExpanded(true);
|
||||||
|
setIsFilterFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
const queryTimeoutEnabled = useCallback(
|
const queryTimeoutEnabled = useCallback(
|
||||||
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||||
[isPreferredApiMongoDB],
|
[isPreferredApiMongoDB],
|
||||||
@@ -1344,6 +1381,19 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedColumnIds,
|
selectedColumnIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const onHideFilterClick = (): void => {
|
||||||
|
setIsFilterExpanded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCloseButtonKeyDown: KeyboardEventHandler<HTMLSpanElement> = (event) => {
|
||||||
|
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||||
|
onHideFilterClick();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
|
||||||
setDocumentIds(newDocumentsIds);
|
setDocumentIds(newDocumentsIds);
|
||||||
|
|
||||||
@@ -1485,9 +1535,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
if (e.key === Constants.NormalizedEventKey.Enter) {
|
if (e.key === "Enter") {
|
||||||
onApplyFilterClick();
|
onApplyFilterClick();
|
||||||
|
|
||||||
|
// Suppress the default behavior of the key
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
onHideFilterClick();
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
// Suppress the default behavior of the key
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -1659,7 +1714,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||||
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -1970,7 +2025,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
setLastFilterContents(limitedLastFilterContents);
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
@@ -1985,6 +2040,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
applyFilterButtonPressed,
|
applyFilterButtonPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// collapse filter
|
||||||
|
setAppliedFilter(filterContent);
|
||||||
|
setIsFilterExpanded(false);
|
||||||
|
|
||||||
// If apply filter is pressed, reset current selected document
|
// If apply filter is pressed, reset current selected document
|
||||||
if (applyFilterButtonPressed) {
|
if (applyFilterButtonPressed) {
|
||||||
setClickedRowIndex(RESET_INDEX);
|
setClickedRowIndex(RESET_INDEX);
|
||||||
@@ -2027,7 +2086,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
setSelectedColumnIds(newSelectedColumnIds);
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
|
||||||
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||||
selectedColumnIds: newSelectedColumnIds,
|
selectedColumnIds: newSelectedColumnIds,
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
});
|
});
|
||||||
@@ -2056,143 +2115,174 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
||||||
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
||||||
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint("bulkdelete");
|
||||||
const isBulkDeleteDisabled =
|
const isBulkDeleteDisabled =
|
||||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|
||||||
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
|
||||||
const options: InputDatalistDropdownOptionSection[] = [];
|
|
||||||
const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== "");
|
|
||||||
if (nonBlankLastFilters.length > 0) {
|
|
||||||
options.push({
|
|
||||||
label: "Saved filters",
|
|
||||||
options: nonBlankLastFilters,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
options.push({
|
|
||||||
label: "Default filters",
|
|
||||||
options: isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
|
||||||
});
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFilterContent = (filter: string): void => {
|
|
||||||
if (filter === "" || filter === undefined) {
|
|
||||||
deleteDocumentsTabSubComponentState(SubComponentName.CurrentFilter, _collection);
|
|
||||||
} else {
|
|
||||||
saveDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, filter, true);
|
|
||||||
}
|
|
||||||
setFilterContent(filter);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CosmosFluentProvider className={styles.container}>
|
<CosmosFluentProvider className={styles.container}>
|
||||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||||
<div className={styles.filterRow}>
|
{isFilterCreated && (
|
||||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
<>
|
||||||
<InputDataList
|
{!isFilterExpanded && !isPreferredApiMongoDB && (
|
||||||
dropdownOptions={getFilterChoices()}
|
<div className={styles.filterRow}>
|
||||||
placeholder={
|
<span>SELECT * FROM c</span>
|
||||||
isPreferredApiMongoDB
|
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
||||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
||||||
: "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."
|
Edit Filter
|
||||||
}
|
</Button>
|
||||||
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}
|
|
||||||
>
|
|
||||||
{!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>
|
</div>
|
||||||
{tableItems.length > 0 && (
|
)}
|
||||||
<a
|
{!isFilterExpanded && isPreferredApiMongoDB && (
|
||||||
className={styles.loadMore}
|
<div className={styles.filterRow}>
|
||||||
role="button"
|
{appliedFilter.length > 0 && <span>Filter :</span>}
|
||||||
tabIndex={0}
|
{!(appliedFilter.length > 0) && <span className="noFilterApplied">No filter applied</span>}
|
||||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
<span className={styles.appliedFilter}>{appliedFilter}</span>
|
||||||
onKeyDown={onLoadMoreKeyInput}
|
<Button appearance="primary" size="small" onClick={onShowFilterClick}>
|
||||||
>
|
Edit Filter
|
||||||
Load more
|
</Button>
|
||||||
</a>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{isFilterExpanded && (
|
||||||
</Allotment.Pane>
|
<div className={styles.filterRow}>
|
||||||
<Allotment.Pane minSize={30}>
|
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||||
<div style={{ height: "100%", width: "100%" }}>
|
<Input
|
||||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
ref={filterInput}
|
||||||
<EditorReact
|
type="text"
|
||||||
language={"json"}
|
size="small"
|
||||||
content={selectedDocumentContent}
|
list={`filtersList-${getUniqueId(_collection)}`}
|
||||||
isReadOnly={false}
|
className={`filterInput ${styles.filterInput}`}
|
||||||
ariaLabel={"Document editor"}
|
title="Type a query predicate or choose one from the list."
|
||||||
lineNumbers={"on"}
|
placeholder={
|
||||||
theme={"_theme"}
|
isPreferredApiMongoDB
|
||||||
onContentChanged={_onEditorContentChange}
|
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||||
enableWordWrapContextMenuItem={true}
|
: "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)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{selectedRows.size > 1 && (
|
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
||||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
{addStringsNoDuplicate(
|
||||||
)}
|
lastFilterContents,
|
||||||
</div>
|
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
||||||
</Allotment.Pane>
|
).map((filter) => (
|
||||||
</Allotment>
|
<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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{bulkDeleteOperation && (
|
{bulkDeleteOperation && (
|
||||||
<ProgressModalDialog
|
<ProgressModalDialog
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { deleteDocuments } from "Common/MongoProxyClient";
|
import { deleteDocuments } from "Common/MongoProxyClient";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import {
|
import {
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
DISCARD_BUTTON_ID,
|
DISCARD_BUTTON_ID,
|
||||||
@@ -67,13 +67,6 @@ jest.mock("Explorer/Controls/Dialog", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Added as recent change to @azure/core-util would cause randomUUID() to throw an error during jest tests.
|
|
||||||
// TODO: when not using beta version of @azure/cosmos sdk try removing this
|
|
||||||
jest.mock("@azure/core-util", () => ({
|
|
||||||
...jest.requireActual("@azure/core-util"),
|
|
||||||
randomUUID: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||||
let newWrapper;
|
let newWrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -170,7 +163,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -180,7 +173,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
@@ -195,7 +188,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocuments).toHaveBeenCalled();
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe("DocumentsTableComponent", () => {
|
|||||||
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
|
{ [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" },
|
||||||
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
|
{ [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" },
|
||||||
],
|
],
|
||||||
|
onItemClicked: (): void => {},
|
||||||
onSelectedRowsChange: (): void => {},
|
onSelectedRowsChange: (): void => {},
|
||||||
selectedRows: new Set<TableRowId>(),
|
selectedRows: new Set<TableRowId>(),
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -38,13 +38,14 @@ import {
|
|||||||
TextSortDescendingRegular,
|
TextSortDescendingRegular,
|
||||||
} from "@fluentui/react-icons";
|
} from "@fluentui/react-icons";
|
||||||
import { NormalizedEventKey } from "Common/Constants";
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
|
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
||||||
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
ColumnSort,
|
ColumnSort,
|
||||||
deleteDocumentsTabSubComponentState,
|
deleteSubComponentState,
|
||||||
readDocumentsTabSubComponentState,
|
readSubComponentState,
|
||||||
saveDocumentsTabSubComponentState,
|
saveSubComponentState,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
@@ -68,6 +69,7 @@ export type ColumnDefinition = {
|
|||||||
export interface IDocumentsTableComponentProps {
|
export interface IDocumentsTableComponentProps {
|
||||||
onRefreshTable: () => void;
|
onRefreshTable: () => void;
|
||||||
items: DocumentsTableComponentItem[];
|
items: DocumentsTableComponentItem[];
|
||||||
|
onItemClicked: (index: number) => void;
|
||||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
size: { height: number; width: number };
|
size: { height: number; width: number };
|
||||||
@@ -97,7 +99,6 @@ const defaultSize = {
|
|||||||
idealWidth: 200,
|
idealWidth: 200,
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
onRefreshTable,
|
onRefreshTable,
|
||||||
items,
|
items,
|
||||||
@@ -115,14 +116,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
const sortedRowsRef = React.useRef(null);
|
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
|
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||||
SubComponentName.ColumnSizes,
|
|
||||||
collection,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
selectedColumnIds.forEach((columnId) => {
|
selectedColumnIds.forEach((columnId) => {
|
||||||
if (
|
if (
|
||||||
@@ -146,7 +141,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
sortDirection: "ascending" | "descending";
|
sortDirection: "ascending" | "descending";
|
||||||
sortColumn: TableColumnId | undefined;
|
sortColumn: TableColumnId | undefined;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||||
|
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
return {
|
return {
|
||||||
@@ -178,12 +173,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as ColumnSizesMap);
|
}, {} as ColumnSizesMap);
|
||||||
|
|
||||||
saveDocumentsTabSubComponentState<ColumnSizesMap>(
|
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
||||||
SubComponentName.ColumnSizes,
|
|
||||||
collection,
|
|
||||||
persistentSizes,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return newSizingOptions;
|
return newSizingOptions;
|
||||||
});
|
});
|
||||||
@@ -195,14 +185,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
setColumnSort(event, columnId, direction);
|
setColumnSort(event, columnId, direction);
|
||||||
|
|
||||||
if (columnId === undefined || direction === undefined) {
|
if (columnId === undefined || direction === undefined) {
|
||||||
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
|
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, {
|
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
||||||
columnId,
|
|
||||||
direction,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
@@ -241,29 +228,31 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||||
Refresh
|
Refresh
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<>
|
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) && (
|
||||||
<MenuItem
|
<>
|
||||||
icon={<TextSortAscendingRegular />}
|
<MenuItem
|
||||||
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
icon={<TextSortAscendingRegular />}
|
||||||
>
|
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||||
Sort ascending
|
>
|
||||||
</MenuItem>
|
Sort ascending
|
||||||
<MenuItem
|
|
||||||
icon={<TextSortDescendingRegular />}
|
|
||||||
onClick={(e) => onSortClick(e, column.id, "descending")}
|
|
||||||
>
|
|
||||||
Sort descending
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
|
||||||
Reset sorting
|
|
||||||
</MenuItem>
|
|
||||||
{!isColumnSelectionDisabled && (
|
|
||||||
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
|
||||||
Edit columns
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
<MenuItem
|
||||||
<MenuDivider />
|
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 />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="keyboardresize"
|
key="keyboardresize"
|
||||||
icon={<TableResizeColumnRegular />}
|
icon={<TableResizeColumnRegular />}
|
||||||
@@ -271,24 +260,25 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
>
|
>
|
||||||
Resize with left/right arrow keys
|
Resize with left/right arrow keys
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!isColumnSelectionDisabled && (
|
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) &&
|
||||||
<MenuItem
|
!isColumnSelectionDisabled && (
|
||||||
key="remove"
|
<MenuItem
|
||||||
icon={<DeleteRegular />}
|
key="remove"
|
||||||
onClick={() => {
|
icon={<DeleteRegular />}
|
||||||
// Remove column id from selectedColumnIds
|
onClick={() => {
|
||||||
const index = selectedColumnIds.indexOf(column.id);
|
// Remove column id from selectedColumnIds
|
||||||
if (index === -1) {
|
const index = selectedColumnIds.indexOf(column.id);
|
||||||
return;
|
if (index === -1) {
|
||||||
}
|
return;
|
||||||
const newSelectedColumnIds = [...selectedColumnIds];
|
}
|
||||||
newSelectedColumnIds.splice(index, 1);
|
const newSelectedColumnIds = [...selectedColumnIds];
|
||||||
onColumnSelectionChange(newSelectedColumnIds);
|
newSelectedColumnIds.splice(index, 1);
|
||||||
}}
|
onColumnSelectionChange(newSelectedColumnIds);
|
||||||
>
|
}}
|
||||||
Remove column
|
>
|
||||||
</MenuItem>
|
Remove column
|
||||||
)}
|
</MenuItem>
|
||||||
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</MenuPopover>
|
</MenuPopover>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -305,42 +295,22 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
|
|
||||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
||||||
const onTableCellClicked = useCallback(
|
const onTableCellClicked = useCallback(
|
||||||
(e: React.MouseEvent | undefined, index: number, rowId: TableRowId) => {
|
(e: React.MouseEvent, index: number) => {
|
||||||
if (isSelectionDisabled) {
|
if (isSelectionDisabled) {
|
||||||
// Only allow click
|
// Only allow click
|
||||||
onSelectedRowsChange(new Set<TableRowId>([rowId]));
|
onSelectedRowsChange(new Set<TableRowId>([index]));
|
||||||
setSelectionStartIndex(index);
|
setSelectionStartIndex(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The selection helper computes in the index space (what's visible to the user in the table, ie the sorted array).
|
|
||||||
// selectedRows is in the rowId space (the index of the original unsorted array), so it must be converted to the index space.
|
|
||||||
const selectedRowsIndex = new Set<number>();
|
|
||||||
selectedRows.forEach((rowId) => {
|
|
||||||
const index = sortedRowsRef.current.findIndex((row: TableRowData) => row.rowId === rowId);
|
|
||||||
if (index !== -1) {
|
|
||||||
selectedRowsIndex.add(index);
|
|
||||||
} else {
|
|
||||||
// This should never happen
|
|
||||||
console.error(`Row with rowId ${rowId} not found in sorted rows`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = selectionHelper(
|
const result = selectionHelper(
|
||||||
selectedRowsIndex,
|
selectedRows as Set<number>,
|
||||||
index,
|
index,
|
||||||
e && isEnvironmentShiftPressed(e),
|
isEnvironmentShiftPressed(e),
|
||||||
e && isEnvironmentCtrlPressed(e),
|
isEnvironmentCtrlPressed(e),
|
||||||
selectionStartIndex,
|
selectionStartIndex,
|
||||||
);
|
);
|
||||||
|
onSelectedRowsChange(result.selection);
|
||||||
// Convert selectionHelper result from index space back to rowId space
|
|
||||||
const selectedRowIds = new Set<TableRowId>();
|
|
||||||
result.selection.forEach((index) => {
|
|
||||||
selectedRowIds.add(sortedRowsRef.current[index].rowId);
|
|
||||||
});
|
|
||||||
onSelectedRowsChange(selectedRowIds);
|
|
||||||
|
|
||||||
if (result.selectionStartIndex !== undefined) {
|
if (result.selectionStartIndex !== undefined) {
|
||||||
setSelectionStartIndex(result.selectionStartIndex);
|
setSelectionStartIndex(result.selectionStartIndex);
|
||||||
}
|
}
|
||||||
@@ -354,20 +324,16 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
* - a key is down and the cell is clicked by the mouse
|
* - a key is down and the cell is clicked by the mouse
|
||||||
*/
|
*/
|
||||||
const onIdClicked = useCallback(
|
const onIdClicked = useCallback(
|
||||||
(e: React.KeyboardEvent<Element>, rowId: TableRowId) => {
|
(e: React.KeyboardEvent<Element>, index: number) => {
|
||||||
if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) {
|
if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) {
|
||||||
onSelectedRowsChange(new Set<TableRowId>([rowId]));
|
onSelectedRowsChange(new Set<TableRowId>([index]));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onSelectedRowsChange],
|
[onSelectedRowsChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => {
|
const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => {
|
||||||
// WARNING: because the table sorts the data, 'index' is not the same as 'rowId'
|
const { item, selected, appearance, onClick, onKeyDown } = data[index];
|
||||||
// The rowId is the index of the item in the original array,
|
|
||||||
// while the index is the index of the item in the sorted array
|
|
||||||
const { item, selected, appearance, onClick, onKeyDown, rowId } = data[index];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
aria-rowindex={index + 2}
|
aria-rowindex={index + 2}
|
||||||
@@ -397,8 +363,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
key={column.columnId}
|
key={column.columnId}
|
||||||
className={styles.tableCell}
|
className={styles.tableCell}
|
||||||
// When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick.
|
// When clicking on a cell with shift/ctrl key, onKeyDown is called instead of onClick.
|
||||||
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index, rowId)}
|
onClick={(e: React.MouseEvent<Element, MouseEvent>) => onTableCellClicked(e, index)}
|
||||||
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, rowId)}
|
onKeyPress={(e: React.KeyboardEvent<Element>) => onIdClicked(e, index)}
|
||||||
{...columnSizing.getTableCellProps(column.columnId)}
|
{...columnSizing.getTableCellProps(column.columnId)}
|
||||||
tabIndex={column.columnId === "id" ? 0 : -1}
|
tabIndex={column.columnId === "id" ? 0 : -1}
|
||||||
>
|
>
|
||||||
@@ -458,19 +424,6 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store the sorted rows in a ref which won't trigger a re-render (as opposed to a state)
|
|
||||||
sortedRowsRef.current = rows;
|
|
||||||
|
|
||||||
// If there are no selected rows, auto select the first row
|
|
||||||
const [autoSelectFirstDoc, setAutoSelectFirstDoc] = React.useState<boolean>(true);
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (autoSelectFirstDoc && sortedRowsRef.current?.length > 0 && selectedRows.size === 0) {
|
|
||||||
setAutoSelectFirstDoc(false);
|
|
||||||
const DOC_INDEX_TO_SELECT = 0;
|
|
||||||
onTableCellClicked(undefined, DOC_INDEX_TO_SELECT, sortedRowsRef.current[DOC_INDEX_TO_SELECT].rowId);
|
|
||||||
}
|
|
||||||
}, [selectedRows, onTableCellClicked, autoSelectFirstDoc]);
|
|
||||||
|
|
||||||
const toggleAllKeydown = React.useCallback(
|
const toggleAllKeydown = React.useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
|
|||||||
@@ -17,124 +17,111 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
SELECT * FROM c
|
SELECT * FROM c
|
||||||
</span>
|
</span>
|
||||||
<InputDataList
|
<span
|
||||||
bottomLink={
|
className="___r7kt3y0_0000000 fqerorx"
|
||||||
{
|
|
||||||
"text": "Learn more",
|
|
||||||
"url": "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dropdownOptions={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"label": "Default filters",
|
|
||||||
"options": [
|
|
||||||
"WHERE c.id = "foo"",
|
|
||||||
"ORDER BY c._ts DESC",
|
|
||||||
"WHERE c.id = "foo" ORDER BY c._ts DESC",
|
|
||||||
"ORDER BY c._ts ASC",
|
|
||||||
"WHERE c.foo = "foo"",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
onChange={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
placeholder="Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
|
||||||
title="Type a query predicate or choose one from the list."
|
|
||||||
value=""
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
aria-label="Apply filter"
|
|
||||||
disabled={false}
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
size="small"
|
size="small"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
Apply Filter
|
Edit Filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Allotment
|
<div
|
||||||
onDragEnd={[Function]}
|
style={
|
||||||
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"overflow": "hidden",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Allotment.Pane
|
<Allotment
|
||||||
minSize={55}
|
onDragEnd={[Function]}
|
||||||
preferredSize="35%"
|
|
||||||
>
|
>
|
||||||
<div
|
<Allotment.Pane
|
||||||
style={
|
minSize={55}
|
||||||
{
|
preferredSize="35%"
|
||||||
"height": "100%",
|
|
||||||
"overflow": "hidden",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
style={
|
||||||
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
className="___9o87uj0_0000000 ffefeo0"
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "calc(100% + -11px)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DocumentsTableComponent
|
<div
|
||||||
collection={
|
style={
|
||||||
{
|
{
|
||||||
"databaseId": "databaseId",
|
"height": "100%",
|
||||||
"id": [Function],
|
"width": "calc(100% + -11px)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
columnDefinitions={
|
>
|
||||||
[
|
<DocumentsTableComponent
|
||||||
|
collection={
|
||||||
{
|
{
|
||||||
"id": "id",
|
"databaseId": "databaseId",
|
||||||
"isPartitionKey": false,
|
"id": [Function],
|
||||||
"label": "id",
|
}
|
||||||
},
|
}
|
||||||
]
|
columnDefinitions={
|
||||||
}
|
[
|
||||||
defaultColumnSelection={
|
{
|
||||||
[
|
"id": "id",
|
||||||
"id",
|
"isPartitionKey": false,
|
||||||
]
|
"label": "id",
|
||||||
}
|
},
|
||||||
isColumnSelectionDisabled={false}
|
]
|
||||||
isRowSelectionDisabled={true}
|
}
|
||||||
items={[]}
|
defaultColumnSelection={
|
||||||
onColumnSelectionChange={[Function]}
|
[
|
||||||
onRefreshTable={[Function]}
|
"id",
|
||||||
onSelectedRowsChange={[Function]}
|
]
|
||||||
selectedColumnIds={
|
}
|
||||||
[
|
isColumnSelectionDisabled={false}
|
||||||
"id",
|
isRowSelectionDisabled={true}
|
||||||
]
|
items={[]}
|
||||||
}
|
onColumnSelectionChange={[Function]}
|
||||||
selectedRows={Set {}}
|
onItemClicked={[Function]}
|
||||||
/>
|
onRefreshTable={[Function]}
|
||||||
|
onSelectedRowsChange={[Function]}
|
||||||
|
selectedColumnIds={
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedRows={
|
||||||
|
Set {
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Allotment.Pane>
|
||||||
</Allotment.Pane>
|
<Allotment.Pane
|
||||||
<Allotment.Pane
|
minSize={30}
|
||||||
minSize={30}
|
>
|
||||||
>
|
<div
|
||||||
<div
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"height": "100%",
|
||||||
"height": "100%",
|
"width": "100%",
|
||||||
"width": "100%",
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Allotment.Pane>
|
||||||
</Allotment.Pane>
|
</Allotment>
|
||||||
</Allotment>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
onItemClicked={[Function]}
|
||||||
onRefreshTable={[Function]}
|
onRefreshTable={[Function]}
|
||||||
onSelectedRowsChange={[Function]}
|
onSelectedRowsChange={[Function]}
|
||||||
selectedColumnIds={[]}
|
selectedColumnIds={[]}
|
||||||
@@ -503,6 +504,7 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
onItemClicked={[Function]}
|
||||||
onRefreshTable={[Function]}
|
onRefreshTable={[Function]}
|
||||||
onSelectedRowsChange={[Function]}
|
onSelectedRowsChange={[Function]}
|
||||||
selectedColumnIds={[]}
|
selectedColumnIds={[]}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ActionType, TabKind } from "Contracts/ActionContracts";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import MongoUtility from "../../../Common/MongoUtility";
|
import MongoUtility from "../../../Common/MongoUtility";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
@@ -21,7 +20,7 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
private mongoQueryTabProps: IMongoQueryTabProps,
|
private mongoQueryTabProps: IMongoQueryTabProps,
|
||||||
) {
|
) {
|
||||||
super(options, mongoQueryTabProps);
|
super(options, mongoQueryTabProps);
|
||||||
this.queryText = options.queryText ?? "";
|
this.queryText = "";
|
||||||
this.iMongoQueryTabComponentProps = {
|
this.iMongoQueryTabComponentProps = {
|
||||||
collection: options.collection,
|
collection: options.collection,
|
||||||
isExecutionError: this.isExecutionError(),
|
isExecutionError: this.isExecutionError(),
|
||||||
@@ -29,8 +28,6 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
tabsBaseInstance: this,
|
tabsBaseInstance: this,
|
||||||
queryText: this.queryText,
|
queryText: this.queryText,
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
splitterDirection: options.splitterDirection,
|
|
||||||
queryViewSizePercent: options.queryViewSizePercent,
|
|
||||||
container: this.mongoQueryTabProps.container,
|
container: this.mongoQueryTabProps.container,
|
||||||
onTabAccessor: (instance: ITabAccessor): void => {
|
onTabAccessor: (instance: ITabAccessor): void => {
|
||||||
this.iTabAccessor = instance;
|
this.iTabAccessor = instance;
|
||||||
@@ -38,26 +35,6 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
isPreferredApiMongoDB: true,
|
isPreferredApiMongoDB: true,
|
||||||
monacoEditorSetting: "plaintext",
|
monacoEditorSetting: "plaintext",
|
||||||
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
||||||
onUpdatePersistedState: (state: {
|
|
||||||
queryText: string;
|
|
||||||
splitterDirection: string;
|
|
||||||
queryViewSizePercent: number;
|
|
||||||
}): void => {
|
|
||||||
this.persistedState = {
|
|
||||||
actionType: ActionType.OpenCollectionTab,
|
|
||||||
tabKind: TabKind.SQLQuery,
|
|
||||||
databaseResourceId: options.collection.databaseId,
|
|
||||||
collectionResourceId: options.collection.id(),
|
|
||||||
query: {
|
|
||||||
text: state.queryText,
|
|
||||||
},
|
|
||||||
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
|
||||||
queryViewSizePercent: state.queryViewSizePercent,
|
|
||||||
};
|
|
||||||
if (this.triggerPersistState) {
|
|
||||||
this.triggerPersistState();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
constructor(props: IMongoShellTabComponentProps) {
|
constructor(props: IMongoShellTabComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._logTraces = new Map();
|
this._logTraces = new Map();
|
||||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
ariaLabel: saveLabel,
|
ariaLabel: saveLabel,
|
||||||
children: saveButtonChildren.length && [
|
children: saveButtonChildren.length && [
|
||||||
{
|
{
|
||||||
iconName: "Save",
|
|
||||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||||
commandButtonLabel: saveLabel,
|
commandButtonLabel: saveLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
|
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
@@ -27,8 +26,6 @@ export class NewQueryTab extends TabsBase {
|
|||||||
public iQueryTabComponentProps: IQueryTabComponentProps;
|
public iQueryTabComponentProps: IQueryTabComponentProps;
|
||||||
public iTabAccessor: ITabAccessor;
|
public iTabAccessor: ITabAccessor;
|
||||||
|
|
||||||
protected persistedState: OpenQueryTab;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
options: QueryTabOptions,
|
options: QueryTabOptions,
|
||||||
private props: IQueryTabProps,
|
private props: IQueryTabProps,
|
||||||
@@ -42,41 +39,12 @@ export class NewQueryTab extends TabsBase {
|
|||||||
tabsBaseInstance: this,
|
tabsBaseInstance: this,
|
||||||
queryText: options.queryText,
|
queryText: options.queryText,
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
splitterDirection: options.splitterDirection,
|
|
||||||
queryViewSizePercent: options.queryViewSizePercent,
|
|
||||||
container: this.props.container,
|
container: this.props.container,
|
||||||
onTabAccessor: (instance: ITabAccessor): void => {
|
onTabAccessor: (instance: ITabAccessor): void => {
|
||||||
this.iTabAccessor = instance;
|
this.iTabAccessor = instance;
|
||||||
},
|
},
|
||||||
isPreferredApiMongoDB: false,
|
isPreferredApiMongoDB: false,
|
||||||
onUpdatePersistedState: (state: {
|
|
||||||
queryText: string;
|
|
||||||
splitterDirection: string;
|
|
||||||
queryViewSizePercent: number;
|
|
||||||
}): void => {
|
|
||||||
this.persistedState = {
|
|
||||||
actionType: ActionType.OpenCollectionTab,
|
|
||||||
tabKind: TabKind.SQLQuery,
|
|
||||||
databaseResourceId: options.collection.databaseId,
|
|
||||||
collectionResourceId: options.collection.id(),
|
|
||||||
query: {
|
|
||||||
text: state.queryText,
|
|
||||||
},
|
|
||||||
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
|
||||||
queryViewSizePercent: state.queryViewSizePercent,
|
|
||||||
};
|
|
||||||
if (this.triggerPersistState) {
|
|
||||||
this.triggerPersistState();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// set initial state
|
|
||||||
this.iQueryTabComponentProps.onUpdatePersistedState({
|
|
||||||
queryText: options.queryText,
|
|
||||||
splitterDirection: options.splitterDirection,
|
|
||||||
queryViewSizePercent: options.queryViewSizePercent,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|||||||
AppStateComponentNames: {
|
AppStateComponentNames: {
|
||||||
QueryCopilot: "QueryCopilot",
|
QueryCopilot: "QueryCopilot",
|
||||||
},
|
},
|
||||||
readSubComponentState: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { SplitterDirection } from "Common/Splitter";
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
@@ -18,7 +19,13 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
|||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
import {
|
||||||
|
LocalStorageUtility,
|
||||||
|
StorageKey,
|
||||||
|
getDefaultQueryResultsView,
|
||||||
|
getRUThreshold,
|
||||||
|
ruThresholdEnabled,
|
||||||
|
} from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -48,7 +55,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
|||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
@@ -93,13 +99,6 @@ export interface IQueryTabComponentProps {
|
|||||||
copilotEnabled?: boolean;
|
copilotEnabled?: boolean;
|
||||||
isSampleCopilotActive?: boolean;
|
isSampleCopilotActive?: boolean;
|
||||||
copilotStore?: Partial<QueryCopilotState>;
|
copilotStore?: Partial<QueryCopilotState>;
|
||||||
splitterDirection?: "horizontal" | "vertical";
|
|
||||||
queryViewSizePercent?: number;
|
|
||||||
onUpdatePersistedState: (state: {
|
|
||||||
queryText: string;
|
|
||||||
splitterDirection: "vertical" | "horizontal";
|
|
||||||
queryViewSizePercent: number;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueryTabStates {
|
interface IQueryTabStates {
|
||||||
@@ -119,13 +118,11 @@ interface IQueryTabStates {
|
|||||||
queryResultsView: SplitterDirection;
|
queryResultsView: SplitterDirection;
|
||||||
errors?: QueryError[];
|
errors?: QueryError[];
|
||||||
modelMarkers?: monaco.editor.IMarkerData[];
|
modelMarkers?: monaco.editor.IMarkerData[];
|
||||||
queryViewSizePercent: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const copilotStore = useCopilotStore();
|
const copilotStore = useCopilotStore();
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const queryTabProps = {
|
const queryTabProps = {
|
||||||
...props,
|
...props,
|
||||||
@@ -135,12 +132,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
|
|||||||
isSampleCopilotActive: isSampleCopilotActive,
|
isSampleCopilotActive: isSampleCopilotActive,
|
||||||
copilotStore: copilotStore,
|
copilotStore: copilotStore,
|
||||||
};
|
};
|
||||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />;
|
return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />;
|
return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
||||||
@@ -149,8 +146,6 @@ type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
|||||||
|
|
||||||
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
|
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
|
||||||
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
||||||
private static readonly DEBOUNCE_DELAY_MS = 1000;
|
|
||||||
|
|
||||||
public queryEditorId: string;
|
public queryEditorId: string;
|
||||||
public executeQueryButton: Button;
|
public executeQueryButton: Button;
|
||||||
public saveQueryButton: Button;
|
public saveQueryButton: Button;
|
||||||
@@ -162,10 +157,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _iterator: MinimalQueryIterator;
|
private _iterator: MinimalQueryIterator;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
queryEditor: React.RefObject<EditorReact>;
|
queryEditor: React.RefObject<EditorReact>;
|
||||||
private timeoutId: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
constructor(props: QueryTabComponentImplProps) {
|
constructor(props: QueryTabComponentImplProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.queryEditor = createRef<EditorReact>();
|
this.queryEditor = createRef<EditorReact>();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -181,9 +176,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
cancelQueryTimeoutID: undefined,
|
cancelQueryTimeoutID: undefined,
|
||||||
copilotActive: this._queryCopilotActive(),
|
copilotActive: this._queryCopilotActive(),
|
||||||
currentTabActive: true,
|
currentTabActive: true,
|
||||||
queryResultsView:
|
queryResultsView: getDefaultQueryResultsView(),
|
||||||
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
|
|
||||||
queryViewSizePercent: props.queryViewSizePercent,
|
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
@@ -214,23 +207,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to save the query text in the query tab state
|
|
||||||
* Since it reads and writes to the same state, it is debounced
|
|
||||||
*/
|
|
||||||
private saveQueryTabStateDebounced = () => {
|
|
||||||
if (this.timeoutId) {
|
|
||||||
clearTimeout(this.timeoutId);
|
|
||||||
}
|
|
||||||
this.timeoutId = setTimeout(async () => {
|
|
||||||
this.props.onUpdatePersistedState({
|
|
||||||
queryText: this.state.sqlQueryEditorContent,
|
|
||||||
splitterDirection: this.state.queryResultsView,
|
|
||||||
queryViewSizePercent: this.state.queryViewSizePercent,
|
|
||||||
});
|
|
||||||
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
|
|
||||||
};
|
|
||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
@@ -591,7 +567,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
private _setViewLayout(direction: SplitterDirection): void {
|
private _setViewLayout(direction: SplitterDirection): void {
|
||||||
this.setState({ queryResultsView: direction }, () => this.saveQueryTabStateDebounced());
|
this.setState({ queryResultsView: direction });
|
||||||
|
|
||||||
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -623,16 +599,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
if (this.state.copilotActive) {
|
if (this.state.copilotActive) {
|
||||||
this.props.copilotStore?.setQuery(newContent);
|
this.props.copilotStore?.setQuery(newContent);
|
||||||
}
|
}
|
||||||
this.setState(
|
this.setState({
|
||||||
{
|
sqlQueryEditorContent: newContent,
|
||||||
sqlQueryEditorContent: newContent,
|
queryCopilotGeneratedQuery: "",
|
||||||
queryCopilotGeneratedQuery: "",
|
|
||||||
|
|
||||||
// Clear the markers when the user edits the document.
|
// Clear the markers when the user edits the document.
|
||||||
modelMarkers: [],
|
modelMarkers: [],
|
||||||
},
|
});
|
||||||
() => this.saveQueryTabStateDebounced(),
|
|
||||||
);
|
|
||||||
if (this.isPreferredApiMongoDB) {
|
if (this.isPreferredApiMongoDB) {
|
||||||
if (newContent.length > 0) {
|
if (newContent.length > 0) {
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
@@ -731,20 +704,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)}
|
||||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||||
<Allotment
|
<Allotment key={vertical.toString()} vertical={vertical}>
|
||||||
key={vertical.toString()}
|
<Allotment.Pane data-test="QueryTab/EditorPane">
|
||||||
vertical={vertical}
|
|
||||||
onDragEnd={(sizes: number[]) => {
|
|
||||||
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
|
||||||
this.setState({ queryViewSizePercent }, () => this.saveQueryTabStateDebounced());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Allotment.Pane
|
|
||||||
data-test="QueryTab/EditorPane"
|
|
||||||
preferredSize={
|
|
||||||
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<EditorReact
|
<EditorReact
|
||||||
ref={this.queryEditor}
|
ref={this.queryEditor}
|
||||||
className={this.props.styles.queryEditor}
|
className={this.props.styles.queryEditor}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||||
@@ -11,18 +10,6 @@ export class SettingsTabV2 extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
||||||
protected persistedState: OpenCollectionTab;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
|
||||||
super(options);
|
|
||||||
this.persistedState = {
|
|
||||||
actionType: ActionType.OpenCollectionTab,
|
|
||||||
tabKind: TabKind.ScaleSettings,
|
|
||||||
databaseResourceId: options.collection.databaseId,
|
|
||||||
collectionResourceId: options.collection.id(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public onActivate(): void {
|
public onActivate(): void {
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -15,7 +16,6 @@ import { useTabs } from "../../../hooks/useTabs";
|
|||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import ScriptTabBase from "../ScriptTabBase";
|
import ScriptTabBase from "../ScriptTabBase";
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react";
|
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { IpRule } from "Contracts/DataModels";
|
import { IpRule } from "Contracts/DataModels";
|
||||||
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
|
import { CommandBarV2 } from "Explorer/Menus/CommandBarV2/CommandBarV2";
|
||||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||||
@@ -33,7 +36,7 @@ interface TabsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||||
const [
|
const [
|
||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
@@ -58,6 +61,29 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
|
{networkSettingsWarning && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.warning}
|
||||||
|
styles={defaultMessageBarStyles}
|
||||||
|
actions={
|
||||||
|
<MessageBarButton
|
||||||
|
onClick={() =>
|
||||||
|
sendMessage({
|
||||||
|
type:
|
||||||
|
userContext.apiType === "VCoreMongo"
|
||||||
|
? MessageTypes.OpenVCoreMongoNetworkingBlade
|
||||||
|
: MessageTypes.OpenPostgresNetworkingBlade,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Change network settings
|
||||||
|
</MessageBarButton>
|
||||||
|
}
|
||||||
|
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||||
|
>
|
||||||
|
{networkSettingsWarning}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -81,6 +107,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabPanesContainer">
|
<div className="tabPanesContainer">
|
||||||
|
{userContext.features.commandBarV2 && <CommandBarV2 explorer={explorer} />}
|
||||||
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
|
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
|
||||||
{openedTabs.map((tab) => (
|
{openedTabs.map((tab) => (
|
||||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||||
@@ -203,7 +230,7 @@ const CloseButton = ({
|
|||||||
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
|
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
|
||||||
>
|
>
|
||||||
<span className="tabIcon close-Icon">
|
<span className="tabIcon close-Icon">
|
||||||
<img src={errorIcon} title="Close" alt="Close" aria-label="hidden" />
|
<img src={errorIcon} title="Close" alt="Close" role="none" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -318,7 +345,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||||
if (
|
if (
|
||||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
|
||||||
(userContext.apiType === "Cassandra" &&
|
(userContext.apiType === "Cassandra" &&
|
||||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||||
ipRules?.length
|
ipRules?.length
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OpenTab } from "Contracts/ActionContracts";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -10,7 +10,6 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
|||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
// TODO: Use specific actions for logging telemetry data
|
// TODO: Use specific actions for logging telemetry data
|
||||||
@@ -31,8 +30,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
protected _theme: string;
|
protected _theme: string;
|
||||||
public onLoadStartKey: number;
|
public onLoadStartKey: number;
|
||||||
|
|
||||||
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
|
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
super();
|
super();
|
||||||
this.index = options.index;
|
this.index = options.index;
|
||||||
@@ -58,10 +55,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by useTabs to persist
|
|
||||||
public getPersistedState = (): OpenTab | null => this.persistedState;
|
|
||||||
public triggerPersistState: () => void = undefined;
|
|
||||||
|
|
||||||
public onCloseTabButtonClick(): void {
|
public onCloseTabButtonClick(): void {
|
||||||
useTabs.getState().closeTab(this);
|
useTabs.getState().closeTab(this);
|
||||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
@@ -14,7 +15,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import TriggerTab from "./TriggerTab";
|
import TriggerTab from "./TriggerTab";
|
||||||
|
|
||||||
const triggerTypeOptions: IDropdownOption[] = [
|
const triggerTypeOptions: IDropdownOption[] = [
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user