mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-30 14:22:05 +00:00
Compare commits
3 Commits
release/ma
...
users/aisa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0437553b | ||
|
|
c181d92e84 | ||
|
|
f3501d8cb8 |
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>
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -61,8 +61,6 @@
|
|||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
@LinkColor: #2d6da4;
|
|
||||||
|
|
||||||
//Icons
|
//Icons
|
||||||
@InfoIconColor: #0072c6;
|
@InfoIconColor: #0072c6;
|
||||||
@WarningIconColor: #db7500;
|
@WarningIconColor: #db7500;
|
||||||
@@ -248,10 +246,6 @@
|
|||||||
outline: 1px dashed @FocusColor;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusedBorder() {
|
|
||||||
border: 1px dashed @FocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
Common Toggle Switch
|
Common Toggle Switch
|
||||||
*************************************************************************************************/
|
*************************************************************************************************/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ a:focus {
|
|||||||
|
|
||||||
#divExplorer {
|
#divExplorer {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
padding: @FabricBoxMargin;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
@@ -37,12 +38,12 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
@@ -67,16 +68,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 +97,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 +122,7 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -156,21 +159,24 @@ 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-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 +202,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%);
|
||||||
}
|
}
|
||||||
@@ -210,4 +218,4 @@ a:focus {
|
|||||||
|
|
||||||
.fileImportImg img {
|
.fileImportImg img {
|
||||||
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
11
package.json
11
package.json
@@ -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",
|
||||||
@@ -246,4 +247,4 @@
|
|||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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://cdb-ms-mpac-pbe.cosmos.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://cdb-ms-mpac-pbe.cosmos.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 {
|
||||||
@@ -138,15 +130,6 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendApi {
|
|
||||||
public static readonly GenerateToken: string = "GenerateToken";
|
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7235";
|
public static readonly Development: string = "https://localhost:7235";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||||
@@ -163,18 +146,6 @@ export class MongoProxyEndpoints {
|
|||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoProxyApi {
|
|
||||||
public static readonly ResourceList: string = "ResourceList";
|
|
||||||
public static readonly QueryDocuments: string = "QueryDocuments";
|
|
||||||
public static readonly CreateDocument: string = "CreateDocument";
|
|
||||||
public static readonly ReadDocument: string = "ReadDocument";
|
|
||||||
public static readonly UpdateDocument: string = "UpdateDocument";
|
|
||||||
public static readonly DeleteDocument: string = "DeleteDocument";
|
|
||||||
public static readonly CreateCollectionWithProxy: string = "CreateCollectionWithProxy";
|
|
||||||
public static readonly LegacyMongoShell: string = "LegacyMongoShell";
|
|
||||||
public static readonly BulkDelete: string = "BulkDelete";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CassandraProxyEndpoints {
|
export class CassandraProxyEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7240";
|
public static readonly Development: string = "https://localhost:7240";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||||
@@ -183,18 +154,6 @@ export class CassandraProxyEndpoints {
|
|||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Remove this when new backend is migrated over
|
|
||||||
export class CassandraBackend {
|
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
|
||||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
|
||||||
public static readonly queryApi: string = "api/cassandra";
|
|
||||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
|
||||||
public static readonly keysApi: string = "api/cassandra/keys";
|
|
||||||
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
|
|
||||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
|
||||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CassandraProxyAPIs {
|
export class CassandraProxyAPIs {
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||||
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
|
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
|
||||||
@@ -525,18 +484,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 class MaterializedViewsLabels {
|
|
||||||
public static readonly NewMaterializedView: string = "New Materialized View";
|
|
||||||
}
|
|
||||||
export class FeedbackLabels {
|
|
||||||
public static readonly provideFeedback: string = "Provide feedback";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|
||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { 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 { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
|
||||||
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";
|
||||||
@@ -20,7 +18,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
@@ -43,7 +41,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
switch (requestInfo.resourceType) {
|
switch (requestInfo.resourceType) {
|
||||||
case Cosmos.ResourceType.conflicts:
|
case Cosmos.ResourceType.conflicts:
|
||||||
case Cosmos.ResourceType.container:
|
case Cosmos.ResourceType.container:
|
||||||
@@ -55,13 +53,8 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
// User resource tokens
|
// User resource tokens
|
||||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||||
const resourceTokens = (
|
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||||
).resourceTokenInfo.resourceTokens;
|
|
||||||
checkDatabaseResourceTokensValidity(
|
|
||||||
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
|
||||||
.resourceTokenInfo.resourceTokensTimestamp,
|
|
||||||
);
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
case Cosmos.ResourceType.none:
|
||||||
@@ -72,9 +65,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||||
// (which is a valid token, but won't work for these operations).
|
// (which is a valid token, but won't work for these operations).
|
||||||
const resourceTokens2 = (
|
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
|
||||||
).resourceTokenInfo.resourceTokens;
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||||
@@ -165,24 +156,13 @@ 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;
|
||||||
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.ConnectionString ||
|
userContext.authType === AuthType.ConnectionString ||
|
||||||
|
|||||||
@@ -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,16 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMaterializedViewsEnabled(): boolean {
|
|
||||||
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
|
|||||||
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";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import {
|
||||||
|
deleteDocument,
|
||||||
|
getEndpoint,
|
||||||
|
getFeatureEndpointOrDefault,
|
||||||
|
queryDocuments,
|
||||||
|
readDocument,
|
||||||
|
updateDocument,
|
||||||
|
} from "./MongoProxyClient";
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -188,8 +196,20 @@ describe("MongoProxyClient", () => {
|
|||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds the correct proxy URL in development", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe("deleteDocuments", () => {
|
describe("deleteDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -206,9 +226,9 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -218,9 +238,9 @@ describe("MongoProxyClient", () => {
|
|||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -255,4 +275,33 @@ describe("MongoProxyClient", () => {
|
|||||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFeatureEndpointOrDefault", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||||
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
|
});
|
||||||
|
const features = extractFeatures(params);
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.AAD,
|
||||||
|
features: features,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a local endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a production endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -141,7 +141,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -183,7 +183,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -229,7 +229,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -250,6 +250,46 @@ export function updateDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
databaseID: databaseId,
|
||||||
|
collectionID: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
resourceID: rid,
|
||||||
|
resourceType: "docs",
|
||||||
|
subscriptionID: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseAccountName: databaseAccount.name,
|
||||||
|
partitionKey:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(endpoint, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "deleting document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteDocuments(
|
export function deleteDocuments(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
@@ -275,7 +315,7 @@ export function deleteDocuments(
|
|||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -319,7 +359,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -339,6 +379,11 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFeatureEndpointOrDefault(): string {
|
||||||
|
const endpoint: string = configContext.MONGO_PROXY_ENDPOINT;
|
||||||
|
return getEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
export function getEndpoint(endpoint: string): string {
|
export function getEndpoint(endpoint: string): string {
|
||||||
let url = endpoint + "/api/mongo/explorer";
|
let url = endpoint + "/api/mongo/explorer";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||||
|
|
||||||
export function updateStyles(): void {
|
export function updateStyles(): void {
|
||||||
if (isFabric()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { useDatabases } from "../../Explorer/useDatabases";
|
import { useDatabases } from "../../Explorer/useDatabases";
|
||||||
@@ -25,7 +24,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
let collection: DataModels.Collection;
|
let collection: DataModels.Collection;
|
||||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.createNewDatabase) {
|
if (params.createNewDatabase) {
|
||||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||||
@@ -100,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: {
|
||||||
@@ -274,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 };
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
|
||||||
import { Collection, CreateMaterializedViewsParams } from "Contracts/DataModels";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
|
|
||||||
import {
|
|
||||||
CreateUpdateOptions,
|
|
||||||
SqlContainerResource,
|
|
||||||
SqlDatabaseCreateUpdateParameters,
|
|
||||||
} from "Utils/arm/generatedClients/cosmos/types";
|
|
||||||
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
|
||||||
|
|
||||||
export const createMaterializedView = async (params: CreateMaterializedViewsParams): Promise<Collection> => {
|
|
||||||
const clearMessage = logConsoleProgress(
|
|
||||||
`Creating a new materialized view ${params.materializedViewId} for database ${params.databaseId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
|
||||||
|
|
||||||
const resource: SqlContainerResource = {
|
|
||||||
id: params.materializedViewId,
|
|
||||||
};
|
|
||||||
if (params.materializedViewDefinition) {
|
|
||||||
resource.materializedViewDefinition = params.materializedViewDefinition;
|
|
||||||
}
|
|
||||||
if (params.analyticalStorageTtl) {
|
|
||||||
resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
|
||||||
}
|
|
||||||
if (params.indexingPolicy) {
|
|
||||||
resource.indexingPolicy = params.indexingPolicy;
|
|
||||||
}
|
|
||||||
if (params.partitionKey) {
|
|
||||||
resource.partitionKey = params.partitionKey;
|
|
||||||
}
|
|
||||||
if (params.uniqueKeyPolicy) {
|
|
||||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
|
||||||
}
|
|
||||||
if (params.vectorEmbeddingPolicy) {
|
|
||||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
|
||||||
}
|
|
||||||
if (params.fullTextPolicy) {
|
|
||||||
resource.fullTextPolicy = params.fullTextPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
|
||||||
properties: {
|
|
||||||
resource,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const createResponse = await createUpdateSqlContainer(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId,
|
|
||||||
params.materializedViewId,
|
|
||||||
rpPayload,
|
|
||||||
);
|
|
||||||
logConsoleInfo(`Successfully created materialized view ${params.materializedViewId}`);
|
|
||||||
|
|
||||||
return createResponse && (createResponse.properties.resource as Collection);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
@@ -13,7 +12,7 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||||
try {
|
try {
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
await deleteCollectionWithARM(databaseId, collectionId);
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).delete();
|
await client().database(databaseId).container(collectionId).delete();
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
? parseInt(resource.softAllowedMaximumThroughput)
|
? parseInt(resource.softAllowedMaximumThroughput)
|
||||||
: resource.softAllowedMaximumThroughput;
|
: resource.softAllowedMaximumThroughput;
|
||||||
|
|
||||||
const throughputBuckets = resource?.throughputBuckets;
|
|
||||||
|
|
||||||
if (autoscaleSettings) {
|
if (autoscaleSettings) {
|
||||||
return {
|
return {
|
||||||
id: offerId,
|
id: offerId,
|
||||||
@@ -116,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ContainerResponse } from "@azure/cosmos";
|
import { ContainerResponse } from "@azure/cosmos";
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
@@ -17,13 +16,15 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
|
||||||
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
if (
|
||||||
|
configContext.platform === Platform.Fabric &&
|
||||||
|
userContext.fabricContext &&
|
||||||
|
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||||
|
) {
|
||||||
const collections: DataModels.Collection[] = [];
|
const collections: DataModels.Collection[] = [];
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
for (const collectionResourceId in (
|
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
|
||||||
).resourceTokenInfo.resourceTokens) {
|
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
const tokenDatabaseId = resourceIdObj[1];
|
const tokenDatabaseId = resourceIdObj[1];
|
||||||
@@ -55,8 +56,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
return await readCollectionsWithARM(databaseId);
|
return await readCollectionsWithARM(databaseId);
|
||||||
}
|
}
|
||||||
@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
|||||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
return rpResponse?.value?.map((collection: any) => {
|
|
||||||
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
|
|
||||||
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
|
|
||||||
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
|
|
||||||
return collectionDataModel;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -11,9 +11,8 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
if (isFabricMirroredKey() || isFabricNative()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||||
// For Fabric Native, it is not supported.
|
|
||||||
console.error("Skiping readDatabaseOffer for Fabric");
|
console.error("Skiping readDatabaseOffer for Fabric");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -24,8 +23,7 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
return await readDatabaseOfferWithARM(params.databaseId);
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
@@ -15,13 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||||
isFabricMirroredKey() &&
|
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
|
||||||
.resourceTokens
|
|
||||||
) {
|
|
||||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
|
||||||
.resourceTokenInfo;
|
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
@@ -52,28 +46,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
}));
|
}));
|
||||||
clearMessage();
|
clearMessage();
|
||||||
return databases;
|
return databases;
|
||||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
|
||||||
const databaseId = userContext.fabricContext.databaseName;
|
|
||||||
databases = [
|
|
||||||
{
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: databaseId,
|
|
||||||
collections: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
clearMessage();
|
|
||||||
return databases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
databases = await readDatabasesWithARM();
|
databases = await readDatabasesWithARM();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Collection } from "../../Contracts/DataModels";
|
import { Collection } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -37,8 +36,7 @@ export async function updateCollection(
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import {
|
||||||
migrateCassandraKeyspaceToAutoscale,
|
migrateCassandraKeyspaceToAutoscale,
|
||||||
@@ -57,7 +56,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.collectionId) {
|
if (params.collectionId) {
|
||||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||||
} else if (userContext.apiType === "Tables") {
|
} else if (userContext.apiType === "Tables") {
|
||||||
@@ -360,13 +359,6 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
body.properties.resource.throughput = params.manualThroughput;
|
body.properties.resource.throughput = params.manualThroughput;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.throughputBuckets) {
|
|
||||||
const throughputBuckets = params.throughputBuckets.filter(
|
|
||||||
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
|
|
||||||
);
|
|
||||||
body.properties.resource.throughputBuckets = throughputBuckets;
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
BackendApi,
|
|
||||||
CassandraProxyEndpoints,
|
|
||||||
JunoEndpoints,
|
|
||||||
MongoProxyEndpoints,
|
|
||||||
PortalBackendEndpoints,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
@@ -12,11 +6,12 @@ import {
|
|||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
|
allowedMongoBackendEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
|
||||||
defaultAllowedCassandraProxyEndpoints,
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
defaultAllowedMongoProxyEndpoints,
|
defaultAllowedMongoProxyEndpoints,
|
||||||
|
defaultAllowedPortalBackendEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
@@ -30,7 +25,7 @@ export enum Platform {
|
|||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedPortalBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||||
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
||||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
@@ -50,10 +45,9 @@ export interface ConfigContext {
|
|||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
@@ -72,7 +66,7 @@ export interface ConfigContext {
|
|||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedPortalBackendEndpoints: defaultAllowedPortalBackendEndpoints,
|
||||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||||
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
||||||
allowedParentFrameOrigins: [
|
allowedParentFrameOrigins: [
|
||||||
@@ -88,7 +82,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/",
|
||||||
@@ -109,7 +103,6 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
globallyEnabledCassandraAPIs: [],
|
globallyEnabledCassandraAPIs: [],
|
||||||
@@ -148,6 +141,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.PORTAL_BACKEND_ENDPOINT,
|
||||||
|
configContext.allowedPortalBackendEndpoints || defaultAllowedPortalBackendEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
@@ -157,6 +159,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||||
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
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 {
|
||||||
@@ -32,7 +31,6 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
writeLocations?: DatabaseAccountResponseLocation[];
|
writeLocations?: DatabaseAccountResponseLocation[];
|
||||||
enableFreeTier?: boolean;
|
enableFreeTier?: boolean;
|
||||||
enableAnalyticalStorage?: boolean;
|
enableAnalyticalStorage?: boolean;
|
||||||
enableMaterializedViews?: boolean;
|
|
||||||
isVirtualNetworkFilterEnabled?: boolean;
|
isVirtualNetworkFilterEnabled?: boolean;
|
||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
@@ -161,12 +159,9 @@ 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;
|
||||||
materializedViews?: MaterializedView[];
|
|
||||||
materializedViewDefinition?: MaterializedViewDefinition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionsWithPagination {
|
export interface CollectionsWithPagination {
|
||||||
@@ -204,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 {
|
||||||
@@ -226,17 +213,6 @@ export interface ComputedProperty {
|
|||||||
|
|
||||||
export type ComputedProperties = ComputedProperty[];
|
export type ComputedProperties = ComputedProperty[];
|
||||||
|
|
||||||
export interface MaterializedView {
|
|
||||||
id: string;
|
|
||||||
_rid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MaterializedViewDefinition {
|
|
||||||
definition: string;
|
|
||||||
sourceCollectionId: string;
|
|
||||||
sourceCollectionRid?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartitionKey {
|
export interface PartitionKey {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
kind: "Hash" | "Range" | "MultiHash";
|
kind: "Hash" | "Range" | "MultiHash";
|
||||||
@@ -289,12 +265,6 @@ export interface Offer {
|
|||||||
offerReplacePending: boolean;
|
offerReplacePending: boolean;
|
||||||
instantMaximumThroughput?: number;
|
instantMaximumThroughput?: number;
|
||||||
softAllowedMaximumThroughput?: number;
|
softAllowedMaximumThroughput?: number;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThroughputBucket {
|
|
||||||
id: number;
|
|
||||||
maxThroughputPercentage: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SDKOfferDefinition extends Resource {
|
export interface SDKOfferDefinition extends Resource {
|
||||||
@@ -359,7 +329,9 @@ export interface CreateDatabaseParams {
|
|||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParamsBase {
|
export interface CreateCollectionParams {
|
||||||
|
createNewDatabase: boolean;
|
||||||
|
collectionId: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
@@ -370,17 +342,6 @@ export interface CreateCollectionParamsBase {
|
|||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
createMongoWildcardIndex?: boolean;
|
createMongoWildcardIndex?: boolean;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||||
fullTextPolicy?: FullTextPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
|
||||||
createNewDatabase: boolean;
|
|
||||||
collectionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
|
||||||
materializedViewId: string;
|
|
||||||
materializedViewDefinition: MaterializedViewDefinition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicy {
|
export interface VectorEmbeddingPolicy {
|
||||||
@@ -394,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;
|
||||||
@@ -425,7 +376,6 @@ export interface UpdateOfferParams {
|
|||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
migrateToAutoPilot?: boolean;
|
migrateToAutoPilot?: boolean;
|
||||||
migrateToManual?: boolean;
|
migrateToManual?: boolean;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
@@ -693,5 +643,3 @@ export interface FeatureRegistration {
|
|||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tags = { [key: string]: string };
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
export enum FabricMessageTypes {
|
export enum FabricMessageTypes {
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
GetAuthorizationToken = "GetAuthorizationToken",
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
GetAllResourceTokens = "GetAllResourceTokens",
|
||||||
GetAccessToken = "GetAccessToken",
|
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,47 @@
|
|||||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
// This is the version of these messages
|
||||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
export const FABRIC_RPC_VERSION = "2";
|
||||||
|
|
||||||
// Fabric to Data Explorer
|
// Fabric to Data Explorer
|
||||||
|
|
||||||
|
// TODO Deprecated. Remove this section once DE is updated
|
||||||
|
export type FabricMessageV1 =
|
||||||
|
| {
|
||||||
|
type: "newContainer";
|
||||||
|
databaseName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "initialize";
|
||||||
|
message: {
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
error: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "authorizationToken";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
data: AuthorizationToken | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "allResourceTokens";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
export type FabricMessageV2 =
|
export type FabricMessageV2 =
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
@@ -31,7 +69,7 @@ export type FabricMessageV2 =
|
|||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: ResourceTokenInfo | undefined;
|
data: FabricDatabaseConnectionInfo | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -41,81 +79,17 @@ export type FabricMessageV2 =
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FabricMessageV3 =
|
export type CosmosDBTokenResponse = {
|
||||||
| {
|
token: string;
|
||||||
type: "newContainer";
|
date: string;
|
||||||
databaseName: string;
|
};
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "initialize";
|
|
||||||
version: string;
|
|
||||||
id: string;
|
|
||||||
message: InitializeMessageV3<CosmosDbArtifactType>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "authorizationToken";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: AuthorizationToken | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "allResourceTokens_v2";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: ResourceTokenInfo | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "explorerVisible";
|
|
||||||
message: {
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "accessToken";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: { accessToken: string };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum CosmosDbArtifactType {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
MIRRORED_KEY = "MIRRORED_KEY",
|
|
||||||
MIRRORED_AAD = "MIRRORED_AAD",
|
|
||||||
NATIVE = "NATIVE",
|
|
||||||
}
|
|
||||||
export interface ArtifactConnectionInfo {
|
|
||||||
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
|
|
||||||
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
|
|
||||||
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessTokenConnectionInfo {
|
|
||||||
accessToken: string;
|
|
||||||
databaseName: string;
|
|
||||||
accountEndpoint: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
|
|
||||||
connectionId: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
isReadOnly: boolean;
|
|
||||||
artifactType: T;
|
|
||||||
artifactConnectionInfo: ArtifactConnectionInfo[T];
|
|
||||||
}
|
|
||||||
export interface CosmosDBConnectionInfoResponse {
|
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
resourceTokens: Record<string, string> | undefined;
|
resourceTokens: { [resourceId: string]: string };
|
||||||
accessToken: string | undefined;
|
};
|
||||||
isReadOnly: boolean;
|
|
||||||
credentialType: "Key" | "OAuth2" | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -143,8 +135,6 @@ export interface Collection extends CollectionBase {
|
|||||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
documentIds: ko.ObservableArray<DocumentId>;
|
documentIds: ko.ObservableArray<DocumentId>;
|
||||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||||
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
|
||||||
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
|
||||||
|
|
||||||
cassandraKeys: CassandraTableKeys;
|
cassandraKeys: CassandraTableKeys;
|
||||||
cassandraSchema: CassandraTableKey[];
|
cassandraSchema: CassandraTableKey[];
|
||||||
@@ -159,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;
|
||||||
@@ -325,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 {
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { MaterializedViewsLabels } from "Common/Constants";
|
|
||||||
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
|
||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import {
|
|
||||||
AddMaterializedViewPanel,
|
|
||||||
AddMaterializedViewPanelProps,
|
|
||||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -27,6 +19,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
import Explorer from "./Explorer";
|
import Explorer from "./Explorer";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||||
@@ -48,7 +41,7 @@ export interface DatabaseContextMenuButtonParams {
|
|||||||
* New resource tree (in ReactJS)
|
* New resource tree (in ReactJS)
|
||||||
*/
|
*/
|
||||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +53,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isFabricNative() && (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: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
@@ -152,7 +145,7 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
@@ -170,24 +163,6 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMaterializedViewsEnabled() && !selectedCollection.materializedViewDefinition()) {
|
|
||||||
items.push({
|
|
||||||
label: MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
onClick: () => {
|
|
||||||
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
|
||||||
explorer: container,
|
|
||||||
sourceContainer: selectedCollection,
|
|
||||||
};
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { AuthType } from "AuthType";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { Features } from "Platform/Hosted/extractFeatures";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
@@ -249,42 +247,4 @@ describe("SettingsComponent", () => {
|
|||||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "SQL",
|
|
||||||
features: { enableThroughputBuckets: true } as Features,
|
|
||||||
authType: AuthType.AAD,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
|
||||||
|
|
||||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
|
||||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
|
||||||
expect(isEnabled).toBe(true);
|
|
||||||
|
|
||||||
wrapper.setState({
|
|
||||||
isThroughputBucketsSaveable: true,
|
|
||||||
throughputBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await settingsComponentInstance.onSaveClick();
|
|
||||||
|
|
||||||
expect(updateOffer).toHaveBeenCalledWith({
|
|
||||||
databaseId: collection.databaseId,
|
|
||||||
collectionId: collection.id(),
|
|
||||||
currentOffer: expect.any(Object),
|
|
||||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
|
||||||
manualThroughput: collection.offer().manualThroughput,
|
|
||||||
throughputBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ 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 {
|
|
||||||
ThroughputBucketsComponent,
|
|
||||||
ThroughputBucketsComponentProps,
|
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
|
||||||
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";
|
||||||
@@ -45,10 +41,6 @@ import {
|
|||||||
ConflictResolutionComponentProps,
|
ConflictResolutionComponentProps,
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
import {
|
|
||||||
MaterializedViewComponent,
|
|
||||||
MaterializedViewComponentProps,
|
|
||||||
} from "./SettingsSubComponents/MaterializedViewComponent";
|
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps,
|
MongoIndexingPolicyComponentProps,
|
||||||
@@ -94,8 +86,6 @@ export interface SettingsComponentState {
|
|||||||
wasAutopilotOriginallySet: boolean;
|
wasAutopilotOriginallySet: boolean;
|
||||||
isScaleSaveable: boolean;
|
isScaleSaveable: boolean;
|
||||||
isScaleDiscardable: boolean;
|
isScaleDiscardable: boolean;
|
||||||
throughputBuckets: DataModels.ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
|
||||||
throughputError: string;
|
throughputError: string;
|
||||||
|
|
||||||
timeToLive: TtlType;
|
timeToLive: TtlType;
|
||||||
@@ -114,14 +104,6 @@ export interface SettingsComponentState {
|
|||||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||||
isSubSettingsSaveable: boolean;
|
isSubSettingsSaveable: boolean;
|
||||||
isSubSettingsDiscardable: boolean;
|
isSubSettingsDiscardable: boolean;
|
||||||
isThroughputBucketsSaveable: 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;
|
||||||
@@ -166,11 +148,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowComputedPropertiesEditor: boolean;
|
private shouldShowComputedPropertiesEditor: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
private isMaterializedView: boolean;
|
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
private throughputBucketsEnabled: boolean;
|
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
@@ -184,16 +163,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||||
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.isMaterializedView =
|
|
||||||
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
|
||||||
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;
|
||||||
this.throughputBucketsEnabled =
|
|
||||||
userContext.apiType === "SQL" &&
|
|
||||||
userContext.features.enableThroughputBuckets &&
|
|
||||||
userContext.authType === AuthType.AAD;
|
|
||||||
|
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
// Mongo container with system partition key still treat as "Fixed"
|
||||||
this.isFixedContainer =
|
this.isFixedContainer =
|
||||||
@@ -212,8 +184,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
wasAutopilotOriginallySet: false,
|
wasAutopilotOriginallySet: false,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
throughputBuckets: undefined,
|
|
||||||
throughputBucketsBaseline: undefined,
|
|
||||||
throughputError: undefined,
|
throughputError: undefined,
|
||||||
|
|
||||||
timeToLive: undefined,
|
timeToLive: undefined,
|
||||||
@@ -232,14 +202,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
changeFeedPolicyBaseline: undefined,
|
changeFeedPolicyBaseline: undefined,
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isThroughputBucketsSaveable: false,
|
|
||||||
|
|
||||||
vectorEmbeddingPolicy: undefined,
|
|
||||||
vectorEmbeddingPolicyBaseline: undefined,
|
|
||||||
fullTextPolicy: undefined,
|
|
||||||
fullTextPolicyBaseline: undefined,
|
|
||||||
shouldDiscardContainerPolicies: false,
|
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
|
|
||||||
indexingPolicyContent: undefined,
|
indexingPolicyContent: undefined,
|
||||||
indexingPolicyContentBaseline: undefined,
|
indexingPolicyContentBaseline: undefined,
|
||||||
@@ -345,12 +307,10 @@ 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 ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||||
this.state.isThroughputBucketsSaveable
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,12 +318,10 @@ 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 ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||||
this.state.isThroughputBucketsSaveable
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -443,14 +401,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
throughput: this.state.throughputBaseline,
|
throughput: this.state.throughputBaseline,
|
||||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
|
||||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
|
||||||
timeToLive: this.state.timeToLiveBaseline,
|
timeToLive: this.state.timeToLiveBaseline,
|
||||||
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: [],
|
||||||
@@ -462,14 +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,
|
||||||
isThroughputBucketsSaveable: false,
|
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isMongoIndexingPolicySaveable: false,
|
isMongoIndexingPolicySaveable: false,
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
@@ -497,21 +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 onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
|
|
||||||
this.setState({ isThroughputBucketsSaveable: isSaveable });
|
|
||||||
};
|
|
||||||
|
|
||||||
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 => {
|
||||||
@@ -599,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 });
|
||||||
|
|
||||||
@@ -758,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();
|
||||||
@@ -780,13 +709,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
] as DataModels.ComputedProperties;
|
] as DataModels.ComputedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throughputBuckets = this.offer?.throughputBuckets;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
throughputBaseline: offerThroughput,
|
throughputBaseline: offerThroughput,
|
||||||
throughputBuckets,
|
|
||||||
throughputBucketsBaseline: throughputBuckets,
|
|
||||||
changeFeedPolicy: changeFeedPolicy,
|
changeFeedPolicy: changeFeedPolicy,
|
||||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||||
timeToLive: timeToLive,
|
timeToLive: timeToLive,
|
||||||
@@ -799,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,
|
||||||
@@ -874,10 +795,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({ throughput: newThroughput, throughputError });
|
this.setState({ throughput: newThroughput, throughputError });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
|
||||||
this.setState({ throughputBuckets });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||||
|
|
||||||
@@ -937,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
|
||||||
@@ -959,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 =
|
||||||
@@ -1001,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();
|
||||||
@@ -1011,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,
|
||||||
@@ -1068,24 +977,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
|
||||||
? this.collection.offer().autoscaleMaxThroughput
|
|
||||||
: undefined,
|
|
||||||
manualThroughput: this.collection.offer().manualThroughput
|
|
||||||
? this.collection.offer().manualThroughput
|
|
||||||
: undefined,
|
|
||||||
throughputBuckets: this.state.throughputBuckets,
|
|
||||||
});
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.offer = updatedOffer;
|
|
||||||
this.setState({ isThroughputBucketsSaveable: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
databaseId: this.collection.databaseId,
|
databaseId: this.collection.databaseId,
|
||||||
@@ -1200,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,
|
||||||
@@ -1266,22 +1142,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
|
||||||
currentBuckets: this.state.throughputBuckets,
|
|
||||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
|
||||||
onBucketsChange: this.onThroughputBucketChange,
|
|
||||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
||||||
collection: this.collection,
|
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
@@ -1297,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} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1340,20 +1208,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughputBucketsEnabled) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
|
||||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMaterializedView) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.MaterializedViewTab,
|
|
||||||
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { collection, container } from "../TestUtils";
|
|
||||||
import { MaterializedViewComponent } from "./MaterializedViewComponent";
|
|
||||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
|
||||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
|
||||||
|
|
||||||
describe("MaterializedViewComponent", () => {
|
|
||||||
let testCollection: typeof collection;
|
|
||||||
let testExplorer: typeof container;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testCollection = { ...collection };
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders only the source component when materializedViewDefinition is missing", () => {
|
|
||||||
testCollection.materializedViews([
|
|
||||||
{ id: "view1", _rid: "rid1" },
|
|
||||||
{ id: "view2", _rid: "rid2" },
|
|
||||||
]);
|
|
||||||
testCollection.materializedViewDefinition(null);
|
|
||||||
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(true);
|
|
||||||
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders only the target component when materializedViews is missing", () => {
|
|
||||||
testCollection.materializedViews(null);
|
|
||||||
testCollection.materializedViewDefinition({
|
|
||||||
definition: "SELECT * FROM c WHERE c.id = 1",
|
|
||||||
sourceCollectionId: "source1",
|
|
||||||
sourceCollectionRid: "rid123",
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
|
||||||
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders neither component when both are missing", () => {
|
|
||||||
testCollection.materializedViews(null);
|
|
||||||
testCollection.materializedViewDefinition(null);
|
|
||||||
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
|
||||||
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import React from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
|
||||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
|
||||||
|
|
||||||
export interface MaterializedViewComponentProps {
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection, explorer }) => {
|
|
||||||
const isTargetContainer = !!collection?.materializedViewDefinition();
|
|
||||||
const isSourceContainer = !!collection?.materializedViews();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
|
||||||
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
|
||||||
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
|
|
||||||
<Text>
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
|
||||||
</Link>{" "}
|
|
||||||
about how to define materialized views and how to use them.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} explorer={explorer} />}
|
|
||||||
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { collection, container } from "../TestUtils";
|
|
||||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
|
||||||
|
|
||||||
describe("MaterializedViewSourceComponent", () => {
|
|
||||||
let testCollection: typeof collection;
|
|
||||||
let testExplorer: typeof container;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testCollection = { ...collection };
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the PrimaryButton", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates when new materialized views are provided", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
|
|
||||||
// Simulating an update by modifying the observable directly
|
|
||||||
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
|
|
||||||
|
|
||||||
wrapper.setProps({ collection: testCollection });
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
|
||||||
import { MaterializedViewsLabels } from "Common/Constants";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
|
||||||
import { AddMaterializedViewPanel } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import * as monaco from "monaco-editor";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export interface MaterializedViewSourceComponentProps {
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({
|
|
||||||
collection,
|
|
||||||
explorer,
|
|
||||||
}) => {
|
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
|
||||||
|
|
||||||
const materializedViews = collection?.materializedViews() ?? [];
|
|
||||||
|
|
||||||
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedViews[] with collection id.
|
|
||||||
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
|
|
||||||
let definition = "";
|
|
||||||
let partitionKey: string[] = [];
|
|
||||||
|
|
||||||
useDatabases.getState().databases.find((database) => {
|
|
||||||
const collection = database.collections().find((collection) => collection.id() === viewId);
|
|
||||||
if (collection) {
|
|
||||||
const materializedViewDefinition = collection.materializedViewDefinition();
|
|
||||||
materializedViewDefinition && (definition = materializedViewDefinition.definition);
|
|
||||||
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { definition, partitionKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
//JSON value for the editor using the fetched id and definitions.
|
|
||||||
const jsonValue = JSON.stringify(
|
|
||||||
materializedViews.map((view) => {
|
|
||||||
const { definition, partitionKey } = getViewDetails(view.id);
|
|
||||||
return {
|
|
||||||
name: view.id,
|
|
||||||
partitionKey: partitionKey.join(", "),
|
|
||||||
definition,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize Monaco editor with the computed JSON value.
|
|
||||||
useEffect(() => {
|
|
||||||
let disposed = false;
|
|
||||||
const initMonaco = async () => {
|
|
||||||
const monacoInstance = await loadMonaco();
|
|
||||||
if (disposed || !editorContainerRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
|
||||||
value: jsonValue,
|
|
||||||
language: "json",
|
|
||||||
ariaLabel: "Materialized Views JSON",
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
initMonaco();
|
|
||||||
return () => {
|
|
||||||
disposed = true;
|
|
||||||
editorRef.current?.dispose();
|
|
||||||
};
|
|
||||||
}, [jsonValue]);
|
|
||||||
|
|
||||||
// Update the editor when the jsonValue changes.
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.setValue(jsonValue);
|
|
||||||
}
|
|
||||||
}, [jsonValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
ref={editorContainerRef}
|
|
||||||
style={{
|
|
||||||
height: 250,
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderRadius: 4,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PrimaryButton
|
|
||||||
text="Add view"
|
|
||||||
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
|
||||||
onClick={() =>
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
<AddMaterializedViewPanel explorer={explorer} sourceContainer={collection} />,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Text } from "@fluentui/react";
|
|
||||||
import { Collection } from "Contracts/ViewModels";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { collection } from "../TestUtils";
|
|
||||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
|
||||||
|
|
||||||
describe("MaterializedViewTargetComponent", () => {
|
|
||||||
let testCollection: Collection;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testCollection = {
|
|
||||||
...collection,
|
|
||||||
materializedViewDefinition: collection.materializedViewDefinition,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays the source container ID", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
|
||||||
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays the materialized view definition", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
|
||||||
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Stack, Text } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export interface MaterializedViewTargetComponentProps {
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
|
|
||||||
const materializedViewDefinition = collection?.materializedViewDefinition();
|
|
||||||
|
|
||||||
const textHeadingStyle = {
|
|
||||||
root: { fontWeight: "600", fontSize: 16 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const valueBoxStyle = {
|
|
||||||
root: {
|
|
||||||
backgroundColor: "#f3f3f3",
|
|
||||||
padding: "5px 10px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
|
||||||
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
|
|
||||||
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
|
||||||
<Stack styles={valueBoxStyle}>
|
|
||||||
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
|
|
||||||
<Stack styles={valueBoxStyle}>
|
|
||||||
<Text>{materializedViewDefinition?.definition}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
|
||||||
|
|
||||||
describe("ThroughputBucketsComponent", () => {
|
|
||||||
const mockOnBucketsChange = jest.fn();
|
|
||||||
const mockOnSaveableChange = jest.fn();
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
currentBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
throughputBucketsBaseline: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 40 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
|
||||||
],
|
|
||||||
onBucketsChange: mockOnBucketsChange,
|
|
||||||
onSaveableChange: mockOnSaveableChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the correct number of buckets", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders buckets in the correct order even if input is unordered", () => {
|
|
||||||
const unorderedBuckets = [
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
];
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
|
||||||
|
|
||||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
|
||||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
|
||||||
const oversizedBuckets = [
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 80 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 90 },
|
|
||||||
{ id: 6, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 7, maxThroughputPercentage: 40 },
|
|
||||||
];
|
|
||||||
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
|
||||||
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onBucketsChange when a bucket value changes", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "70" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers onSaveableChange when values change", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "80" } });
|
|
||||||
|
|
||||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates state consistently after multiple changes to different buckets", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
|
|
||||||
const input1 = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input1, { target: { value: "70" } });
|
|
||||||
|
|
||||||
const input2 = screen.getByDisplayValue("60");
|
|
||||||
fireEvent.change(input2, { target: { value: "80" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 80 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets to baseline when currentBuckets are reset", () => {
|
|
||||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input1 = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input1, { target: { value: "70" } });
|
|
||||||
|
|
||||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "50" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
|
||||||
render(
|
|
||||||
<ThroughputBucketsComponent
|
|
||||||
{...defaultProps}
|
|
||||||
currentBuckets={[
|
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
|
||||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
|
||||||
expect(disabledInputs[0]).toBeDisabled();
|
|
||||||
|
|
||||||
const sliders = screen.getAllByRole("slider");
|
|
||||||
expect(sliders.length).toBeGreaterThan(0);
|
|
||||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const toggles = screen.getAllByRole("switch");
|
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ensures default buckets are used when no buckets are provided", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
|
||||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
|
||||||
import { ThroughputBucket } from "Contracts/DataModels";
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
|
||||||
import { isDirty } from "../../SettingsUtils";
|
|
||||||
|
|
||||||
const MAX_BUCKET_SIZES = 5;
|
|
||||||
|
|
||||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export interface ThroughputBucketsComponentProps {
|
|
||||||
currentBuckets: ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: ThroughputBucket[];
|
|
||||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
|
||||||
onSaveableChange: (isSaveable: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
|
||||||
currentBuckets,
|
|
||||||
throughputBucketsBaseline,
|
|
||||||
onBucketsChange,
|
|
||||||
onSaveableChange,
|
|
||||||
}) => {
|
|
||||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
|
||||||
if (!buckets || buckets.length === 0) {
|
|
||||||
return DEFAULT_BUCKETS;
|
|
||||||
}
|
|
||||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
|
||||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return adjustedDefaultBuckets.map(
|
|
||||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
|
||||||
onSaveableChange(false);
|
|
||||||
}, [currentBuckets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
|
||||||
onSaveableChange(isChanged);
|
|
||||||
}, [throughputBuckets]);
|
|
||||||
|
|
||||||
const handleBucketChange = (id: number, newValue: number) => {
|
|
||||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
|
||||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
|
||||||
);
|
|
||||||
setThroughputBuckets(updatedBuckets);
|
|
||||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
|
||||||
settingsChanged && onBucketsChange(updatedBuckets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggle = (id: number, checked: boolean) => {
|
|
||||||
handleBucketChange(id, checked ? 50 : 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
|
||||||
<Label>Throughput Buckets</Label>
|
|
||||||
<Stack>
|
|
||||||
{throughputBuckets?.map((bucket) => (
|
|
||||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
|
||||||
<Slider
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={bucket.maxThroughputPercentage}
|
|
||||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
|
||||||
showValue={false}
|
|
||||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
|
||||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
value={bucket.maxThroughputPercentage.toString()}
|
|
||||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
|
||||||
type="number"
|
|
||||||
suffix="%"
|
|
||||||
styles={{
|
|
||||||
fieldGroup: { width: 80 },
|
|
||||||
}}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
onText="Active"
|
|
||||||
offText="Inactive"
|
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
|
||||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
|
||||||
></Toggle>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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,15 +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
|
|
||||||
| DataModels.ThroughputBucket[];
|
|
||||||
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";
|
||||||
@@ -56,13 +48,6 @@ export enum SettingsV2TabTypes {
|
|||||||
PartitionKeyTab,
|
PartitionKeyTab,
|
||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
|
||||||
MaterializedViewTab,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
|
||||||
VectorPolicyTab,
|
|
||||||
FullTextPolicyTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -169,11 +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)";
|
||||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
|
||||||
return "Throughput Buckets";
|
|
||||||
case SettingsV2TabTypes.MaterializedViewTab:
|
|
||||||
return "Materialized Views (Preview)";
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,17 +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),
|
|
||||||
materializedViews: ko.observable<DataModels.MaterializedView[]>([
|
|
||||||
{ id: "view1", _rid: "rid1" },
|
|
||||||
{ id: "view2", _rid: "rid2" },
|
|
||||||
]),
|
|
||||||
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
|
|
||||||
definition: "SELECT * FROM c WHERE c.id = 1",
|
|
||||||
sourceCollectionId: "source1",
|
|
||||||
sourceCollectionRid: "rid123",
|
|
||||||
}),
|
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,13 +55,10 @@ 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],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -74,7 +71,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
@@ -136,13 +132,10 @@ 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],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -155,7 +148,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
displayedTtlSeconds="5"
|
displayedTtlSeconds="5"
|
||||||
@@ -257,13 +249,10 @@ 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],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -276,7 +265,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
@@ -342,101 +330,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
shouldDiscardComputedProperties={false}
|
shouldDiscardComputedProperties={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
|
||||||
headerText="Materialized Views (Preview)"
|
|
||||||
itemKey="MaterializedViewTab"
|
|
||||||
key="MaterializedViewTab"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginTop": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MaterializedViewComponent
|
|
||||||
collection={
|
|
||||||
{
|
|
||||||
"analyticalStorageTtl": [Function],
|
|
||||||
"changeFeedPolicy": [Function],
|
|
||||||
"computedProperties": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
|
||||||
"container": Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"databaseId": "test",
|
|
||||||
"defaultTtl": [Function],
|
|
||||||
"fullTextPolicy": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
|
||||||
"getDatabase": [Function],
|
|
||||||
"id": [Function],
|
|
||||||
"indexingPolicy": [Function],
|
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
|
||||||
"partitionKey": {
|
|
||||||
"kind": "hash",
|
|
||||||
"paths": [],
|
|
||||||
"version": 2,
|
|
||||||
},
|
|
||||||
"partitionKeyProperties": [
|
|
||||||
"partitionKey",
|
|
||||||
],
|
|
||||||
"readSettings": [Function],
|
|
||||||
"uniqueKeyPolicy": {},
|
|
||||||
"usageSizeInKB": [Function],
|
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PivotItem>
|
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,23 +34,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
let defaultThroughput: number;
|
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFreeTier ||
|
|
||||||
isQuickstart ||
|
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
|
||||||
) {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
|
||||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
|
||||||
} else {
|
|
||||||
defaultThroughput = 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);
|
||||||
@@ -61,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;
|
||||||
@@ -170,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);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
|||||||
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 { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } 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 { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
@@ -35,7 +35,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";
|
||||||
@@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
|||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
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";
|
||||||
@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
|
|||||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
@@ -187,10 +187,6 @@ export default class Explorer {
|
|||||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirrored()) {
|
|
||||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshExplorer();
|
this.refreshExplorer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +278,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(
|
||||||
@@ -351,8 +378,8 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,7 +1134,7 @@ export default class Explorer {
|
|||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: 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();
|
||||||
|
|
||||||
@@ -1131,7 +1158,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshSampleData();
|
await this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
@@ -1156,27 +1183,26 @@ export default class Explorer {
|
|||||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshSampleData(): void {
|
public async refreshSampleData(): Promise<void> {
|
||||||
if (!userContext.sampleDataConnectionInfo) {
|
try {
|
||||||
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection: DataModels.Collection = await readSampleCollection();
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||||
|
if (!databaseId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "Explorer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
|
||||||
if (!databaseId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
readSampleCollection()
|
|
||||||
.then((collection: DataModels.Collection) => {
|
|
||||||
if (!collection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
|
||||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
.flex-direction(@direction: row);
|
.flex-direction(@direction: row);
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.valueCol {
|
.valueCol {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
@@ -59,10 +63,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customTrashIcon {
|
|
||||||
padding-top: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightPaneTrashIconImg {
|
.rightPaneTrashIconImg {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,10 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div className="labelCol">
|
<div className="labelCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
label={index === 0 && "Key"}
|
|
||||||
type="text"
|
type="text"
|
||||||
id="propertyKeyNewVertexPane"
|
id="propertyKeyNewVertexPane"
|
||||||
componentRef={input}
|
componentRef={input}
|
||||||
required
|
aria-required="true"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label={`Enter value for propery ${index + 1}`}
|
aria-label={`Enter value for propery ${index + 1}`}
|
||||||
@@ -154,11 +153,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
|
||||||
<div className="valueCol">
|
<div className="valueCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
label={index === 0 && "Value"}
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -170,8 +169,6 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div>
|
<div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
role="combobox"
|
role="combobox"
|
||||||
label={index === 0 && "Type"}
|
|
||||||
ariaLabel="Type"
|
|
||||||
placeholder="Select an option"
|
placeholder="Select an option"
|
||||||
defaultSelectedKey={data.values[0].type}
|
defaultSelectedKey={data.values[0].type}
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -184,7 +181,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="actionCol">
|
<div className="actionCol">
|
||||||
<div
|
<div
|
||||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`Delete ${data.key}`}
|
aria-label={`Delete ${data.key}`}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
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 { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -93,18 +93,19 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootStyle = isFabric()
|
const rootStyle =
|
||||||
? {
|
configContext.platform === Platform.Fabric
|
||||||
root: {
|
? {
|
||||||
backgroundColor: "transparent",
|
root: {
|
||||||
padding: "2px 8px 0px 8px",
|
backgroundColor: "transparent",
|
||||||
},
|
padding: "2px 8px 0px 8px",
|
||||||
}
|
},
|
||||||
: {
|
}
|
||||||
root: {
|
: {
|
||||||
backgroundColor: backgroundColor,
|
root: {
|
||||||
},
|
backgroundColor: backgroundColor,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
|
|||||||
@@ -37,25 +37,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
it("Button should not be visible for Tables API", () => {
|
||||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
updateUserContext({
|
||||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
databaseAccount: {
|
||||||
//
|
properties: {
|
||||||
// it("Button should not be visible for Tables API", () => {
|
capabilities: [{ name: "EnableTable" }],
|
||||||
// updateUserContext({
|
},
|
||||||
// databaseAccount: {
|
} as DatabaseAccount,
|
||||||
// properties: {
|
});
|
||||||
// capabilities: [{ name: "EnableTable" }],
|
|
||||||
// },
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
// } as DatabaseAccount,
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
// });
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
//
|
);
|
||||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
// const enableAzureSynapseLinkBtn = buttons.find(
|
});
|
||||||
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
|
||||||
// );
|
|
||||||
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
|
||||||
//});
|
|
||||||
|
|
||||||
it("Button should not be visible for Cassandra API", () => {
|
it("Button should not be visible for Cassandra API", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||||
@@ -62,7 +61,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (userContext.apiType === "SQL") {
|
||||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
&:active {
|
&:active {
|
||||||
background-color:@NotificationHigh;
|
background-color:@NotificationHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
.focusedBorder();
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBar {
|
.statusBar {
|
||||||
.dataTypeIcons {
|
.dataTypeIcons {
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setElememntRef = (element: HTMLElement): void => {
|
||||||
|
this.consoleHeaderElement = element;
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const numInProgress = this.state.allConsoleData.filter(
|
const numInProgress = this.state.allConsoleData.filter(
|
||||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
||||||
@@ -97,9 +101,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
role="button"
|
ref={this.setElememntRef}
|
||||||
aria-label="Console"
|
|
||||||
aria-expanded={this.props.isConsoleExpanded}
|
|
||||||
onClick={() => this.expandCollapseConsole()}
|
onClick={() => this.expandCollapseConsole()}
|
||||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -107,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div className="statusBar">
|
<div className="statusBar">
|
||||||
<span className="dataTypeIcons">
|
<span className="dataTypeIcons">
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={LoadingIcon} alt="In progress items" />
|
<img src={LoadingIcon} alt="in progress items" />
|
||||||
<span className="numInProgress">{numInProgress}</span>
|
<span className="numInProgress">{numInProgress}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={ErrorBlackIcon} alt="Error items" />
|
<img src={ErrorBlackIcon} alt="error items" />
|
||||||
<span className="numErroredItems">{numErroredItems}</span>
|
<span className="numErroredItems">{numErroredItems}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={infoBubbleIcon} alt="Info items" />
|
<img src={infoBubbleIcon} alt="info items" />
|
||||||
<span className="numInfoItems">{numInfoItems}</span>
|
<span className="numInfoItems">{numInfoItems}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -127,10 +129,17 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
<div
|
||||||
|
className="expandCollapseButton"
|
||||||
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||||
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,6 +259,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onConsoleWasExpanded = (): void => {
|
private onConsoleWasExpanded = (): void => {
|
||||||
|
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||||
|
this.consoleHeaderElement.focus();
|
||||||
|
}
|
||||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
|
||||||
aria-label="Console"
|
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -24,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="In progress items"
|
alt="in progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -37,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -50,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -74,11 +71,15 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,13 +176,10 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
|
||||||
aria-label="Console"
|
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -194,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="In progress items"
|
alt="in progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -207,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -220,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -246,11 +244,15 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -57,19 +56,6 @@ function openCollectionTab(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isFabricMirrored() &&
|
|
||||||
!(
|
|
||||||
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
|
|
||||||
(
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { AddCollectionPanel } from "./AddCollectionPanel";
|
import { AddCollectionPanel } from "./AddCollectionPanel";
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -21,50 +21,26 @@ 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 { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
|
||||||
import {
|
|
||||||
AllPropertiesIndexed,
|
|
||||||
AnalyticalStorageContent,
|
|
||||||
ContainerVectorPolicyTooltipContent,
|
|
||||||
FullTextPolicyDefault,
|
|
||||||
getPartitionKey,
|
|
||||||
getPartitionKeyName,
|
|
||||||
getPartitionKeyPlaceHolder,
|
|
||||||
getPartitionKeyTooltipText,
|
|
||||||
isFreeTierAccount,
|
|
||||||
isSynapseLinkEnabled,
|
|
||||||
parseUniqueKeys,
|
|
||||||
scrollToSection,
|
|
||||||
SharedDatabaseDefault,
|
|
||||||
shouldShowAnalyticalStoreOptions,
|
|
||||||
UniqueKeysHeader,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { CollectionCreation } from "Shared/Constants";
|
import { CollectionCreation } from "Shared/Constants";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
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";
|
||||||
import "../../Controls/ThroughputInput/ThroughputInput.less";
|
import "../Controls/ThroughputInput/ThroughputInput.less";
|
||||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
||||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||||
|
|
||||||
export interface AddCollectionPanelProps {
|
export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -72,6 +48,40 @@ export interface AddCollectionPanelProps {
|
|||||||
isQuickstart?: boolean;
|
isQuickstart?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [],
|
||||||
|
excludedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
kind: "Range",
|
||||||
|
dataType: "Number",
|
||||||
|
precision: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "Range",
|
||||||
|
dataType: "String",
|
||||||
|
precision: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: [],
|
||||||
|
};
|
||||||
|
|
||||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
vectorEmbeddings: [],
|
vectorEmbeddings: [],
|
||||||
};
|
};
|
||||||
@@ -99,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> {
|
||||||
@@ -124,7 +131,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: getPartitionKey(props.isQuickstart),
|
partitionKey: this.getPartitionKey(),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
@@ -140,9 +147,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
vectorPolicyValidated: true,
|
||||||
fullTextPolicy: FullTextPolicyDefault,
|
|
||||||
fullTextIndexes: [],
|
|
||||||
fullTextPolicyValidated: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +158,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
||||||
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
||||||
scrollToSection("panelContainer");
|
this.scrollToSection("panelContainer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +175,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.state.errorMessage && isFreeTierAccount() && (
|
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||||
messageType="info"
|
messageType="info"
|
||||||
@@ -265,152 +269,150 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
<Stack hidden={userContext.apiType === "Tables"}>
|
||||||
<Stack hidden={userContext.apiType === "Tables"}>
|
<Stack horizontal>
|
||||||
<Stack horizontal>
|
<span className="mandatoryStar">* </span>
|
||||||
<span className="mandatoryStar">* </span>
|
<Text className="panelTextBold" variant="small">
|
||||||
<Text className="panelTextBold" variant="small">
|
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||||
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
</Text>
|
||||||
</Text>
|
<TooltipHost
|
||||||
<TooltipHost
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
true,
|
true,
|
||||||
).toLocaleLowerCase()}.`}
|
).toLocaleLowerCase()}.`}
|
||||||
>
|
/>
|
||||||
<Icon
|
</TooltipHost>
|
||||||
iconName="Info"
|
</Stack>
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
|
||||||
true,
|
|
||||||
).toLocaleLowerCase()}.`}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{configContext.platform !== Platform.Fabric && (
|
{configContext.platform !== Platform.Fabric && (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<div role="radiogroup">
|
<div role="radiogroup">
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={this.state.createNewDatabase}
|
|
||||||
aria-label="Create new database"
|
|
||||||
aria-checked={this.state.createNewDatabase}
|
|
||||||
name="databaseType"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="databaseCreateNew"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Create new</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={!this.state.createNewDatabase}
|
|
||||||
aria-label="Use existing database"
|
|
||||||
aria-checked={!this.state.createNewDatabase}
|
|
||||||
name="databaseType"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Use existing</span>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.createNewDatabase && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<input
|
<input
|
||||||
name="newDatabaseId"
|
className="panelRadioBtn"
|
||||||
id="newDatabaseId"
|
checked={this.state.createNewDatabase}
|
||||||
aria-required
|
aria-label="Create new database"
|
||||||
required
|
aria-checked={this.state.createNewDatabase}
|
||||||
type="text"
|
name="databaseType"
|
||||||
autoComplete="off"
|
type="radio"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
role="radio"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
id="databaseCreateNew"
|
||||||
placeholder="Type a new database id"
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
aria-label="New database id, Type a new database id"
|
|
||||||
autoFocus
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
value={this.state.newDatabaseId}
|
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
this.setState({ newDatabaseId: event.target.value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Create new</span>
|
||||||
|
|
||||||
{!isServerlessAccount() && (
|
<input
|
||||||
<Stack horizontal>
|
className="panelRadioBtn"
|
||||||
<Checkbox
|
checked={!this.state.createNewDatabase}
|
||||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
aria-label="Use existing database"
|
||||||
checked={this.state.isSharedThroughputChecked}
|
aria-checked={!this.state.createNewDatabase}
|
||||||
styles={{
|
name="databaseType"
|
||||||
text: { fontSize: 12 },
|
type="radio"
|
||||||
checkbox: { width: 12, height: 12 },
|
role="radio"
|
||||||
label: { padding: 0, alignItems: "center" },
|
tabIndex={0}
|
||||||
}}
|
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
/>
|
||||||
this.setState({ isSharedThroughputChecked: isChecked })
|
<span className="panelRadioBtnLabel">Use existing</span>
|
||||||
}
|
</div>
|
||||||
/>
|
</Stack>
|
||||||
<TooltipHost
|
)}
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
{this.state.createNewDatabase && (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<input
|
||||||
|
name="newDatabaseId"
|
||||||
|
id="newDatabaseId"
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
|
placeholder="Type a new database id"
|
||||||
|
size={40}
|
||||||
|
className="panelTextField"
|
||||||
|
aria-label="New database id, Type a new database id"
|
||||||
|
autoFocus
|
||||||
|
tabIndex={0}
|
||||||
|
value={this.state.newDatabaseId}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
this.setState({ newDatabaseId: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isServerlessAccount() && (
|
||||||
|
<Stack horizontal>
|
||||||
|
<Checkbox
|
||||||
|
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||||
|
checked={this.state.isSharedThroughputChecked}
|
||||||
|
styles={{
|
||||||
|
text: { fontSize: 12 },
|
||||||
|
checkbox: { width: 12, height: 12 },
|
||||||
|
label: { padding: 0, alignItems: "center" },
|
||||||
|
}}
|
||||||
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||||
|
this.setState({ isSharedThroughputChecked: isChecked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||||
|
true,
|
||||||
|
).toLocaleLowerCase()} within the database.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||||
true,
|
true,
|
||||||
).toLocaleLowerCase()} within the database.`}
|
).toLocaleLowerCase()} within the database.`}
|
||||||
>
|
/>
|
||||||
<Icon
|
</TooltipHost>
|
||||||
iconName="Info"
|
</Stack>
|
||||||
className="panelInfoIcon"
|
)}
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
|
||||||
true,
|
|
||||||
).toLocaleLowerCase()} within the database.`}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={true}
|
isDatabase={true}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={this.isFreeTierAccount()}
|
||||||
isQuickstart={this.props.isQuickstart}
|
isQuickstart={this.props.isQuickstart}
|
||||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||||
this.setState({ isThroughputCapExceeded })
|
this.setState({ isThroughputCapExceeded })
|
||||||
}
|
}
|
||||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{!this.state.createNewDatabase && (
|
{!this.state.createNewDatabase && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
ariaLabel="Choose an existing database"
|
ariaLabel="Choose an existing database"
|
||||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||||
style={{ width: 300, fontSize: 12 }}
|
style={{ width: 300, fontSize: 12 }}
|
||||||
placeholder="Choose an existing database"
|
placeholder="Choose an existing database"
|
||||||
options={this.getDatabaseOptions()}
|
options={this.getDatabaseOptions()}
|
||||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||||
this.setState({ selectedDatabaseId: database.key as string })
|
this.setState({ selectedDatabaseId: database.key as string })
|
||||||
}
|
}
|
||||||
defaultSelectedKey={this.props.databaseId}
|
defaultSelectedKey={this.props.databaseId}
|
||||||
responsiveMode={999}
|
responsiveMode={999}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Separator className="panelSeparator" />
|
<Separator className="panelSeparator" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
@@ -559,14 +561,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{getPartitionKeyName()}
|
{this.getPartitionKeyName()}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={this.getPartitionKeyTooltipText()}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ariaLabel={getPartitionKeyTooltipText()}
|
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -580,8 +585,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
placeholder={getPartitionKeyPlaceHolder()}
|
placeholder={this.getPartitionKeyPlaceHolder()}
|
||||||
aria-label={getPartitionKeyName()}
|
aria-label={this.getPartitionKeyName()}
|
||||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||||
value={this.state.partitionKey}
|
value={this.state.partitionKey}
|
||||||
@@ -619,8 +624,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
tabIndex={index > 0 ? 1 : 0}
|
tabIndex={index > 0 ? 1 : 0}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={getPartitionKeyPlaceHolder(index)}
|
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||||
aria-label={getPartitionKeyName()}
|
aria-label={this.getPartitionKeyName()}
|
||||||
pattern={".*"}
|
pattern={".*"}
|
||||||
title={""}
|
title={""}
|
||||||
value={subPartitionKey}
|
value={subPartitionKey}
|
||||||
@@ -646,7 +651,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
@@ -711,10 +716,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={this.isFreeTierAccount()}
|
||||||
isQuickstart={this.props.isQuickstart}
|
isQuickstart={this.props.isQuickstart}
|
||||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||||
@@ -727,9 +732,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack>
|
<Stack>
|
||||||
{UniqueKeysHeader()}
|
<Stack horizontal>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Unique keys
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={
|
||||||
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={
|
||||||
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
||||||
@@ -777,18 +802,31 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text className="panelTextBold" variant="small">
|
<Stack horizontal>
|
||||||
{AnalyticalStorageContent()}
|
<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">
|
||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={this.state.enableAnalyticalStore}
|
checked={this.state.enableAnalyticalStore}
|
||||||
disabled={!isSynapseLinkEnabled()}
|
disabled={!this.isSynapseLinkEnabled()}
|
||||||
aria-label="Enable analytical store"
|
aria-label="Enable analytical store"
|
||||||
aria-checked={this.state.enableAnalyticalStore}
|
aria-checked={this.state.enableAnalyticalStore}
|
||||||
name="analyticalStore"
|
name="analyticalStore"
|
||||||
@@ -803,7 +841,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.enableAnalyticalStore}
|
checked={!this.state.enableAnalyticalStore}
|
||||||
disabled={!isSynapseLinkEnabled()}
|
disabled={!this.isSynapseLinkEnabled()}
|
||||||
aria-label="Disable analytical store"
|
aria-label="Disable analytical store"
|
||||||
aria-checked={!this.state.enableAnalyticalStore}
|
aria-checked={!this.state.enableAnalyticalStore}
|
||||||
name="analyticalStore"
|
name="analyticalStore"
|
||||||
@@ -817,7 +855,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
{!this.isSynapseLinkEnabled() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Azure Synapse Link is required for creating an analytical store{" "}
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
@@ -825,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
|
||||||
@@ -847,15 +884,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<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[],
|
||||||
@@ -869,41 +906,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{this.shouldShowFullTextSearchParameters() && (
|
{userContext.apiType !== "Tables" && (
|
||||||
<Stack>
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Container Full Text Search Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
scrollToSection("collapsibleAdvancedSectionContent");
|
this.scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
@@ -1013,6 +1022,31 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPartitionKeyName(isLowerCase?: boolean): string {
|
||||||
|
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||||
|
|
||||||
|
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPartitionKeyPlaceHolder(index?: number): string {
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "Mongo":
|
||||||
|
return "e.g., categoryId";
|
||||||
|
case "Gremlin":
|
||||||
|
return "e.g., /address";
|
||||||
|
case "SQL":
|
||||||
|
return `${
|
||||||
|
index === undefined
|
||||||
|
? "Required - first partition key e.g., /TenantId"
|
||||||
|
: index === 0
|
||||||
|
? "second partition key e.g., /UserId"
|
||||||
|
: "third partition key e.g., /SessionId"
|
||||||
|
}`;
|
||||||
|
default:
|
||||||
|
return "e.g., /address/zipCode";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
if (event.target.checked && !this.state.createNewDatabase) {
|
if (event.target.checked && !this.state.createNewDatabase) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -1100,12 +1134,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return !!selectedDatabase?.offer();
|
return !!selectedDatabase?.offer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isFreeTierAccount(): boolean {
|
||||||
|
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
}
|
||||||
|
|
||||||
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."
|
||||||
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPartitionKeyTooltipText(): string {
|
||||||
|
if (userContext.apiType === "Mongo") {
|
||||||
|
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let tooltipText = `The ${this.getPartitionKeyName(
|
||||||
|
true,
|
||||||
|
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPartitionKey(): string {
|
||||||
|
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (userContext.features.partitionKeyDefault) {
|
||||||
|
return userContext.apiType === "SQL" ? "/id" : "_id";
|
||||||
|
}
|
||||||
|
if (userContext.features.partitionKeyDefault2) {
|
||||||
|
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||||
|
}
|
||||||
|
if (this.props.isQuickstart) {
|
||||||
|
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private getPartitionKeySubtext(): string {
|
private getPartitionKeySubtext(): string {
|
||||||
if (
|
if (
|
||||||
userContext.features.partitionKeyDefault &&
|
userContext.features.partitionKeyDefault &&
|
||||||
@@ -1117,21 +1187,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: uncomment when learn more text becomes available
|
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||||
// private getContainerFullTextPolicyTooltipContent(): 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
|
||||||
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
impacting the performance of transactional workloads.{" "}
|
||||||
// magna aliqua.{" "}
|
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
||||||
// <Link target="_blank" href="https://aka.ms/CosmosFullTextSearch">
|
Learn more
|
||||||
// Learn more
|
</Link>
|
||||||
// </Link>
|
</Text>
|
||||||
// </Text>
|
);
|
||||||
// );
|
}
|
||||||
// }
|
|
||||||
|
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||||
|
queries.{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isFabricNative() || isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1147,7 +1228,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||||
if (!isFreeTierAccount()) {
|
if (!this.isFreeTierAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,12 +1237,41 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
: this.isSelectedDatabaseSharedThroughput();
|
: this.isSelectedDatabaseSharedThroughput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowVectorSearchParameters() {
|
private shouldShowAnalyticalStoreOptions(): boolean {
|
||||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
if (configContext.platform === Platform.Emulator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "SQL":
|
||||||
|
case "Mongo":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowFullTextSearchParameters() {
|
private isSynapseLinkEnabled(): boolean {
|
||||||
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
if (!userContext.databaseAccount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { properties } = userContext.databaseAccount;
|
||||||
|
if (!properties) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.enableAnalyticalStorage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties.capabilities?.some(
|
||||||
|
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldShowVectorSearchParameters() {
|
||||||
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||||
@@ -1220,27 +1330,20 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTtl(): number {
|
private getAnalyticalStorageTtl(): number {
|
||||||
if (!isSynapseLinkEnabled()) {
|
if (!this.isSynapseLinkEnabled()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldShowAnalyticalStoreOptions()) {
|
if (!this.shouldShowAnalyticalStoreOptions()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1254,6 +1357,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
return Constants.AnalyticalStorageTtl.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scrollToSection(id: string): void {
|
||||||
|
document.getElementById(id)?.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
private getSampleDBName(): string {
|
private getSampleDBName(): string {
|
||||||
const existingSampleDBs = useDatabases
|
const existingSampleDBs = useDatabases
|
||||||
.getState()
|
.getState()
|
||||||
@@ -1288,7 +1395,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
partitionKeyString = "/'$pk'";
|
partitionKeyString = "/'$pk'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
@@ -1316,10 +1423,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,
|
||||||
@@ -1379,7 +1482,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,217 +0,0 @@
|
|||||||
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
|
|
||||||
export function getPartitionKeyTooltipText(): string {
|
|
||||||
if (userContext.apiType === "Mongo") {
|
|
||||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
|
||||||
}
|
|
||||||
|
|
||||||
let tooltipText = `The ${getPartitionKeyName(
|
|
||||||
true,
|
|
||||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
|
||||||
|
|
||||||
if (userContext.apiType === "SQL") {
|
|
||||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return tooltipText;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartitionKeyName(isLowerCase?: boolean): string {
|
|
||||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
|
||||||
|
|
||||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartitionKeyPlaceHolder(index?: number): string {
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "Mongo":
|
|
||||||
return "e.g., categoryId";
|
|
||||||
case "Gremlin":
|
|
||||||
return "e.g., /address";
|
|
||||||
case "SQL":
|
|
||||||
return `${
|
|
||||||
index === undefined
|
|
||||||
? "Required - first partition key e.g., /TenantId"
|
|
||||||
: index === 0
|
|
||||||
? "second partition key e.g., /UserId"
|
|
||||||
: "third partition key e.g., /SessionId"
|
|
||||||
}`;
|
|
||||||
default:
|
|
||||||
return "e.g., /address/zipCode";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartitionKey(isQuickstart?: boolean): string {
|
|
||||||
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (userContext.features.partitionKeyDefault) {
|
|
||||||
return userContext.apiType === "SQL" ? "/id" : "_id";
|
|
||||||
}
|
|
||||||
if (userContext.features.partitionKeyDefault2) {
|
|
||||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
|
||||||
}
|
|
||||||
if (isQuickstart) {
|
|
||||||
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isFreeTierAccount(): boolean {
|
|
||||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UniqueKeysHeader(): JSX.Element {
|
|
||||||
const tooltipContent =
|
|
||||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack horizontal>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Unique keys
|
|
||||||
</Text>
|
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldShowAnalyticalStoreOptions(): boolean {
|
|
||||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "SQL":
|
|
||||||
case "Mongo":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnalyticalStorageContent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Text variant="small">
|
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
|
|
||||||
the performance of transactional workloads.{" "}
|
|
||||||
<Link
|
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
|
||||||
target="_blank"
|
|
||||||
href="https://aka.ms/analytical-store-overview"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSynapseLinkEnabled(): boolean {
|
|
||||||
if (!userContext.databaseAccount) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { properties } = userContext.databaseAccount;
|
|
||||||
if (!properties) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (properties.enableAnalyticalStorage) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties.capabilities?.some(
|
|
||||||
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollToSection(id: string): void {
|
|
||||||
document.getElementById(id)?.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Text variant="small">
|
|
||||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
|
||||||
queries.{" "}
|
|
||||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
|
|
||||||
if (uniqueKeys?.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
|
|
||||||
uniqueKeys.forEach((uniqueKey: string) => {
|
|
||||||
if (uniqueKey) {
|
|
||||||
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
|
|
||||||
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
|
|
||||||
if (trimmedPaths?.length > 0) {
|
|
||||||
if (userContext.apiType === "Mongo") {
|
|
||||||
trimmedPaths.map((path) => {
|
|
||||||
const transformedPath = path.split(".").join("/");
|
|
||||||
if (transformedPath[0] !== "/") {
|
|
||||||
return "/" + transformedPath;
|
|
||||||
}
|
|
||||||
return transformedPath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return uniqueKeyPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [],
|
|
||||||
excludedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
|
|
||||||
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
|
|
||||||
fullTextPaths: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
kind: "Range",
|
|
||||||
dataType: "Number",
|
|
||||||
precision: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "Range",
|
|
||||||
dataType: "String",
|
|
||||||
precision: -1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
excludedPaths: [],
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
|
||||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
|
||||||
|
|
||||||
export interface AddMVAdvancedComponentProps {
|
|
||||||
useHashV1: boolean;
|
|
||||||
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
}
|
|
||||||
export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => {
|
|
||||||
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
|
||||||
|
|
||||||
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
|
||||||
setUseHashV1(isChecked);
|
|
||||||
setSubPartitionKeys([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Advanced"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection);
|
|
||||||
scrollToSection("collapsibleAdvancedSectionContent");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
|
||||||
<Checkbox
|
|
||||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
|
||||||
checked={useHashV1}
|
|
||||||
styles={{
|
|
||||||
text: { fontSize: 12 },
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
|
||||||
}}
|
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
|
|
||||||
useHashV1CheckboxOnChange(isChecked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text variant="small">
|
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the created
|
|
||||||
container will use a legacy partitioning scheme that supports partition key values of size only up to 101
|
|
||||||
bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
AnalyticalStorageContent,
|
|
||||||
isSynapseLinkEnabled,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
|
||||||
|
|
||||||
export interface AddMVAnalyticalStoreComponentProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
enableAnalyticalStore: boolean;
|
|
||||||
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => {
|
|
||||||
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
|
|
||||||
|
|
||||||
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
|
|
||||||
if (checked && !enableAnalyticalStore) {
|
|
||||||
setEnableAnalyticalStore(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
|
|
||||||
if (checked && enableAnalyticalStore) {
|
|
||||||
setEnableAnalyticalStore(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
{AnalyticalStorageContent()}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<div role="radiogroup">
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={enableAnalyticalStore}
|
|
||||||
disabled={!isSynapseLinkEnabled()}
|
|
||||||
aria-label="Enable analytical store"
|
|
||||||
aria-checked={enableAnalyticalStore}
|
|
||||||
name="analyticalStore"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="enableAnalyticalStoreBtn"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">On</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={!enableAnalyticalStore}
|
|
||||||
disabled={!isSynapseLinkEnabled()}
|
|
||||||
aria-label="Disable analytical store"
|
|
||||||
aria-checked={!enableAnalyticalStore}
|
|
||||||
name="analyticalStore"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="disableAnalyticalStoreBtn"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Off</span>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Text variant="small">
|
|
||||||
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
|
|
||||||
Enable Synapse Link for this Cosmos DB account.{" "}
|
|
||||||
<Link
|
|
||||||
href="https://aka.ms/cosmosdb-synapselink"
|
|
||||||
target="_blank"
|
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
|
||||||
className="capacitycalculator-link"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
<DefaultButton
|
|
||||||
text="Enable"
|
|
||||||
onClick={() => explorer.openEnableSynapseLinkDialog()}
|
|
||||||
style={{ height: 27, width: 80 }}
|
|
||||||
styles={{ label: { fontSize: 12 } }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
|
||||||
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
|
|
||||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
|
||||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AddMVFullTextSearchComponentProps {
|
|
||||||
fullTextPolicy: FullTextPolicy;
|
|
||||||
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
|
||||||
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
|
||||||
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => {
|
|
||||||
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Container Full Text Search Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
|
||||||
<FullTextPoliciesComponent
|
|
||||||
fullTextPolicy={fullTextPolicy}
|
|
||||||
onFullTextPathChange={(
|
|
||||||
fullTextPolicy: FullTextPolicy,
|
|
||||||
fullTextIndexes: FullTextIndex[],
|
|
||||||
fullTextPolicyValidated: boolean,
|
|
||||||
) => {
|
|
||||||
setFullTextPolicy(fullTextPolicy);
|
|
||||||
setFullTextIndexes(fullTextIndexes);
|
|
||||||
setFullTextPolicyValidated(fullTextPolicyValidated);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import {
|
|
||||||
getPartitionKeyName,
|
|
||||||
getPartitionKeyPlaceHolder,
|
|
||||||
getPartitionKeyTooltipText,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AddMVPartitionKeyComponentProps {
|
|
||||||
partitionKey?: string;
|
|
||||||
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
subPartitionKeys: string[];
|
|
||||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
useHashV1: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVPartitionKeyComponent = (props: AddMVPartitionKeyComponentProps): JSX.Element => {
|
|
||||||
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
|
||||||
|
|
||||||
const partitionKeyValueOnChange = (value: string): void => {
|
|
||||||
if (!partitionKey && !value.startsWith("/")) {
|
|
||||||
setPartitionKey("/" + value);
|
|
||||||
} else {
|
|
||||||
setPartitionKey(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
|
|
||||||
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
|
|
||||||
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
|
|
||||||
updatedSubPartitionKeys[index] = "/" + value.trim();
|
|
||||||
} else {
|
|
||||||
updatedSubPartitionKeys[index] = value.trim();
|
|
||||||
}
|
|
||||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Partition key
|
|
||||||
</Text>
|
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="addmaterializedView-partitionKeyValue"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
placeholder={getPartitionKeyPlaceHolder()}
|
|
||||||
aria-label={getPartitionKeyName()}
|
|
||||||
pattern=".*"
|
|
||||||
value={partitionKey}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
partitionKeyValueOnChange(event.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
|
|
||||||
return (
|
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "20px",
|
|
||||||
border: "solid",
|
|
||||||
borderWidth: "0px 0px 1px 1px",
|
|
||||||
marginRight: "5px",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="addMaterializedView-partitionKeyValue"
|
|
||||||
key={`addMaterializedView-partitionKeyValue_${subPartitionKeyIndex}`}
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
size={40}
|
|
||||||
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
|
|
||||||
className="panelTextField"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
|
|
||||||
aria-label={getPartitionKeyName()}
|
|
||||||
pattern={".*"}
|
|
||||||
title={""}
|
|
||||||
value={subPartitionKey}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ height: 27 }}
|
|
||||||
onClick={() => {
|
|
||||||
const updatedSubPartitionKeys = subPartitionKeys.filter(
|
|
||||||
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
|
|
||||||
);
|
|
||||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<DefaultButton
|
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
|
||||||
hidden={useHashV1}
|
|
||||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
|
||||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
|
||||||
>
|
|
||||||
Add hierarchical partition key
|
|
||||||
</DefaultButton>
|
|
||||||
{subPartitionKeys.length > 0 && (
|
|
||||||
<Text variant="small">
|
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
|
|
||||||
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
|
|
||||||
JavaScript V3 SDK.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { Checkbox, Stack } from "@fluentui/react";
|
|
||||||
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
|
|
||||||
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import React from "react";
|
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
|
||||||
|
|
||||||
export interface AddMVThroughputComponentProps {
|
|
||||||
enableDedicatedThroughput: boolean;
|
|
||||||
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isSelectedSourceContainerSharedThroughput: () => boolean;
|
|
||||||
showCollectionThroughputInput: () => boolean;
|
|
||||||
materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void;
|
|
||||||
isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void;
|
|
||||||
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => {
|
|
||||||
const {
|
|
||||||
enableDedicatedThroughput,
|
|
||||||
setEnabledDedicatedThroughput,
|
|
||||||
isSelectedSourceContainerSharedThroughput,
|
|
||||||
showCollectionThroughputInput,
|
|
||||||
materializedViewThroughputOnChange,
|
|
||||||
isMaterializedViewAutoscaleOnChange,
|
|
||||||
setIsThroughputCapExceeded,
|
|
||||||
isCostAknowledgedOnChange,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
|
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<Checkbox
|
|
||||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
|
||||||
checked={enableDedicatedThroughput}
|
|
||||||
styles={{
|
|
||||||
text: { fontSize: 12 },
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, alignItems: "center" },
|
|
||||||
}}
|
|
||||||
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{showCollectionThroughputInput() && (
|
|
||||||
<ThroughputInput
|
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
|
|
||||||
isDatabase={false}
|
|
||||||
isSharded={false}
|
|
||||||
isFreeTier={isFreeTierAccount()}
|
|
||||||
isQuickstart={false}
|
|
||||||
setThroughputValue={(throughput: number) => {
|
|
||||||
materializedViewThroughputOnChange(throughput);
|
|
||||||
}}
|
|
||||||
setIsAutoscale={(isAutoscale: boolean) => {
|
|
||||||
isMaterializedViewAutoscaleOnChange(isAutoscale);
|
|
||||||
}}
|
|
||||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
|
||||||
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
|
||||||
}}
|
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
|
||||||
isCostAknowledgedOnChange(isAcknowledged);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { ActionButton, IconButton, Stack } from "@fluentui/react";
|
|
||||||
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
|
|
||||||
export interface AddMVUniqueKeysComponentProps {
|
|
||||||
uniqueKeys: string[];
|
|
||||||
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVUniqueKeysComponent = (props: AddMVUniqueKeysComponentProps): JSX.Element => {
|
|
||||||
const { uniqueKeys, setUniqueKeys } = props;
|
|
||||||
|
|
||||||
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
|
||||||
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
|
|
||||||
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return uniqueKey;
|
|
||||||
});
|
|
||||||
setUniqueKeys(updatedUniqueKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
|
|
||||||
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
|
|
||||||
setUniqueKeys(updatedUniqueKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addUniqueKeyOnClick = (): void => {
|
|
||||||
setUniqueKeys([...uniqueKeys, ""]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
{UniqueKeysHeader()}
|
|
||||||
|
|
||||||
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={
|
|
||||||
userContext.apiType === "Mongo"
|
|
||||||
? "Comma separated paths e.g. firstName,address.zipCode"
|
|
||||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
|
||||||
}
|
|
||||||
className="panelTextField"
|
|
||||||
autoFocus
|
|
||||||
value={uniqueKey}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ height: 27 }}
|
|
||||||
onClick={() => {
|
|
||||||
deleteUniqueKeyOnClick(uniqueKeyIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
iconProps={{ iconName: "Add" }}
|
|
||||||
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
|
|
||||||
onClick={() => {
|
|
||||||
addUniqueKeyOnClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add unique key
|
|
||||||
</ActionButton>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
|
||||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
|
||||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
|
||||||
import {
|
|
||||||
ContainerVectorPolicyTooltipContent,
|
|
||||||
scrollToSection,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AddMVVectorSearchComponentProps {
|
|
||||||
vectorEmbeddingPolicy: VectorEmbedding[];
|
|
||||||
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
|
||||||
vectorIndexingPolicy: VectorIndex[];
|
|
||||||
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
|
||||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => {
|
|
||||||
const {
|
|
||||||
vectorEmbeddingPolicy,
|
|
||||||
setVectorEmbeddingPolicy,
|
|
||||||
vectorIndexingPolicy,
|
|
||||||
setVectorIndexingPolicy,
|
|
||||||
setVectorPolicyValidated,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Container Vector Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
|
||||||
}}
|
|
||||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
|
||||||
>
|
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
|
||||||
<VectorEmbeddingPoliciesComponent
|
|
||||||
vectorEmbeddings={vectorEmbeddingPolicy}
|
|
||||||
vectorIndexes={vectorIndexingPolicy}
|
|
||||||
onVectorEmbeddingChange={(
|
|
||||||
vectorEmbeddingPolicy: VectorEmbedding[],
|
|
||||||
vectorIndexingPolicy: VectorIndex[],
|
|
||||||
vectorPolicyValidated: boolean,
|
|
||||||
) => {
|
|
||||||
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
|
|
||||||
setVectorIndexingPolicy(vectorIndexingPolicy);
|
|
||||||
setVectorPolicyValidated(vectorPolicyValidated);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { shallow, ShallowWrapper } from "enzyme";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
AddMaterializedViewPanel,
|
|
||||||
AddMaterializedViewPanelProps,
|
|
||||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
|
||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
const props: AddMaterializedViewPanelProps = {
|
|
||||||
explorer: new Explorer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("AddMaterializedViewPanel", () => {
|
|
||||||
it("render default panel", () => {
|
|
||||||
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
|
||||||
<AddMaterializedViewPanel {...props} />,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render form", () => {
|
|
||||||
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
|
||||||
<AddMaterializedViewPanel {...props} />,
|
|
||||||
);
|
|
||||||
const form = wrapper.find("form").first();
|
|
||||||
expect(form).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import {
|
|
||||||
DirectionalHint,
|
|
||||||
Dropdown,
|
|
||||||
DropdownMenuItemType,
|
|
||||||
Icon,
|
|
||||||
IDropdownOption,
|
|
||||||
Link,
|
|
||||||
Separator,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TooltipHost,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import { createMaterializedView } from "Common/dataAccess/createMaterializedView";
|
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
|
||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
|
||||||
import { Collection, Database } from "Contracts/ViewModels";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
AllPropertiesIndexed,
|
|
||||||
FullTextPolicyDefault,
|
|
||||||
getPartitionKey,
|
|
||||||
isSynapseLinkEnabled,
|
|
||||||
parseUniqueKeys,
|
|
||||||
scrollToSection,
|
|
||||||
shouldShowAnalyticalStoreOptions,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import {
|
|
||||||
chooseSourceContainerStyle,
|
|
||||||
chooseSourceContainerStyles,
|
|
||||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanelStyles";
|
|
||||||
import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent";
|
|
||||||
import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent";
|
|
||||||
import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent";
|
|
||||||
import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent";
|
|
||||||
import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent";
|
|
||||||
import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent";
|
|
||||||
import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent";
|
|
||||||
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
|
|
||||||
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
|
|
||||||
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { CollectionCreation } from "Shared/Constants";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
|
|
||||||
export interface AddMaterializedViewPanelProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
sourceContainer?: Collection;
|
|
||||||
}
|
|
||||||
export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): JSX.Element => {
|
|
||||||
const { explorer, sourceContainer } = props;
|
|
||||||
|
|
||||||
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
|
|
||||||
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
|
||||||
const [materializedViewId, setMaterializedViewId] = useState<string>();
|
|
||||||
const [definition, setDefinition] = useState<string>();
|
|
||||||
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
|
||||||
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
|
||||||
const [useHashV1, setUseHashV1] = useState<boolean>();
|
|
||||||
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
|
|
||||||
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
|
|
||||||
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
|
|
||||||
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
|
|
||||||
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
|
|
||||||
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
|
|
||||||
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
|
|
||||||
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
|
|
||||||
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
|
|
||||||
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
|
||||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sourceContainerOptions: IDropdownOption[] = [];
|
|
||||||
useDatabases.getState().databases.forEach((database: Database) => {
|
|
||||||
sourceContainerOptions.push({
|
|
||||||
key: database.rid,
|
|
||||||
text: database.id(),
|
|
||||||
itemType: DropdownMenuItemType.Header,
|
|
||||||
});
|
|
||||||
|
|
||||||
database.collections().forEach((collection: Collection) => {
|
|
||||||
const isMaterializedView: boolean = !!collection.materializedViewDefinition();
|
|
||||||
sourceContainerOptions.push({
|
|
||||||
key: collection.rid,
|
|
||||||
text: collection.id(),
|
|
||||||
disabled: isMaterializedView,
|
|
||||||
...(isMaterializedView && {
|
|
||||||
title: "This is a materialized view.",
|
|
||||||
}),
|
|
||||||
data: collection,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setSourceContainerOptions(sourceContainerOptions);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
scrollToSection("panelContainer");
|
|
||||||
}, [errorMessage]);
|
|
||||||
|
|
||||||
let materializedViewThroughput: number;
|
|
||||||
let isMaterializedViewAutoscale: boolean;
|
|
||||||
let isCostAcknowledged: boolean;
|
|
||||||
|
|
||||||
const materializedViewThroughputOnChange = (materializedViewThroughputValue: number): void => {
|
|
||||||
materializedViewThroughput = materializedViewThroughputValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMaterializedViewAutoscaleOnChange = (isMaterializedViewAutoscaleValue: boolean): void => {
|
|
||||||
isMaterializedViewAutoscale = isMaterializedViewAutoscaleValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
|
|
||||||
isCostAcknowledged = isCostAcknowledgedValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectedSourceContainerSharedThroughput = (): boolean => {
|
|
||||||
if (!selectedSourceContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!selectedSourceContainer.getDatabase().offer();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCollectionThroughputInput = (): boolean => {
|
|
||||||
if (isServerlessAccount()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableDedicatedThroughput) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showVectorSearchParameters = (): boolean => {
|
|
||||||
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
|
||||||
};
|
|
||||||
|
|
||||||
const showFullTextSearchParameters = (): boolean => {
|
|
||||||
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnalyticalStorageTtl = (): number => {
|
|
||||||
if (!isSynapseLinkEnabled()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldShowAnalyticalStoreOptions()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableAnalyticalStore) {
|
|
||||||
// TODO: always default to 90 days once the backend hotfix is deployed
|
|
||||||
return userContext.features.ttl90Days
|
|
||||||
? Constants.AnalyticalStorageTtl.Days90
|
|
||||||
: Constants.AnalyticalStorageTtl.Infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateInputs = (): boolean => {
|
|
||||||
if (!selectedSourceContainer) {
|
|
||||||
setErrorMessage("Please select a source container");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
|
||||||
const errorMessage = isMaterializedViewAutoscale
|
|
||||||
? "Please acknowledge the estimated monthly spend."
|
|
||||||
: "Please acknowledge the estimated daily spend.";
|
|
||||||
setErrorMessage(errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (materializedViewThroughput > CollectionCreation.MaxRUPerPartition) {
|
|
||||||
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showVectorSearchParameters()) {
|
|
||||||
if (!vectorPolicyValidated) {
|
|
||||||
setErrorMessage("Please fix errors in container vector policy");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullTextPolicyValidated) {
|
|
||||||
setErrorMessage("Please fix errors in container full text search policy");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
|
||||||
event?.preventDefault();
|
|
||||||
|
|
||||||
if (!validateInputs()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const materializedViewIdTrimmed: string = materializedViewId.trim();
|
|
||||||
|
|
||||||
const materializedViewDefinition: DataModels.MaterializedViewDefinition = {
|
|
||||||
sourceCollectionId: sourceContainer.id(),
|
|
||||||
definition: definition,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyTrimmed: string = partitionKey.trim();
|
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
|
|
||||||
const partitionKeyVersion = useHashV1 ? undefined : 2;
|
|
||||||
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
|
|
||||||
? {
|
|
||||||
paths: [
|
|
||||||
partitionKeyTrimmed,
|
|
||||||
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
|
|
||||||
],
|
|
||||||
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
|
||||||
version: partitionKeyVersion,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
|
|
||||||
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
|
|
||||||
|
|
||||||
if (showVectorSearchParameters()) {
|
|
||||||
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
|
|
||||||
vectorEmbeddingPolicyFinal = {
|
|
||||||
vectorEmbeddings: vectorEmbeddingPolicy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showFullTextSearchParameters()) {
|
|
||||||
indexingPolicy.fullTextIndexes = fullTextIndexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const telemetryData: TelemetryProcessor.TelemetryData = {
|
|
||||||
database: {
|
|
||||||
id: selectedSourceContainer.databaseId,
|
|
||||||
shared: isSelectedSourceContainerSharedThroughput(),
|
|
||||||
},
|
|
||||||
collection: {
|
|
||||||
id: materializedViewIdTrimmed,
|
|
||||||
throughput: materializedViewThroughput,
|
|
||||||
isAutoscale: isMaterializedViewAutoscale,
|
|
||||||
partitionKeyPaths,
|
|
||||||
uniqueKeyPolicy,
|
|
||||||
collectionWithDedicatedThroughput: enableDedicatedThroughput,
|
|
||||||
},
|
|
||||||
subscriptionQuotaId: userContext.quotaId,
|
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
|
||||||
};
|
|
||||||
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
|
|
||||||
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
|
|
||||||
|
|
||||||
let offerThroughput: number;
|
|
||||||
let autoPilotMaxThroughput: number;
|
|
||||||
|
|
||||||
if (!databaseLevelThroughput) {
|
|
||||||
if (isMaterializedViewAutoscale) {
|
|
||||||
autoPilotMaxThroughput = materializedViewThroughput;
|
|
||||||
} else {
|
|
||||||
offerThroughput = materializedViewThroughput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = {
|
|
||||||
materializedViewId: materializedViewIdTrimmed,
|
|
||||||
materializedViewDefinition: materializedViewDefinition,
|
|
||||||
databaseId: selectedSourceContainer.databaseId,
|
|
||||||
databaseLevelThroughput: databaseLevelThroughput,
|
|
||||||
offerThroughput: offerThroughput,
|
|
||||||
autoPilotMaxThroughput: autoPilotMaxThroughput,
|
|
||||||
analyticalStorageTtl: getAnalyticalStorageTtl(),
|
|
||||||
indexingPolicy: indexingPolicy,
|
|
||||||
partitionKey: partitionKeyPaths,
|
|
||||||
uniqueKeyPolicy: uniqueKeyPolicy,
|
|
||||||
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
|
|
||||||
fullTextPolicy: fullTextPolicy,
|
|
||||||
};
|
|
||||||
|
|
||||||
setIsExecuting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createMaterializedView(createMaterializedViewParams);
|
|
||||||
await explorer.refreshAllDatabases();
|
|
||||||
TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, telemetryData, startKey);
|
|
||||||
useSidePanel.getState().closeSidePanel();
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage: string = getErrorMessage(error);
|
|
||||||
setErrorMessage(errorMessage);
|
|
||||||
setShowErrorDetails(true);
|
|
||||||
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
|
||||||
TelemetryProcessor.traceFailure(Action.CreateMaterializedView, failureTelemetryData, startKey);
|
|
||||||
} finally {
|
|
||||||
setIsExecuting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="panelFormWrapper" id="panelMaterializedView" onSubmit={submit}>
|
|
||||||
{errorMessage && (
|
|
||||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
|
||||||
)}
|
|
||||||
<div className="panelMainContent">
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Source container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Dropdown
|
|
||||||
placeholder="Choose source container"
|
|
||||||
options={sourceContainerOptions}
|
|
||||||
defaultSelectedKey={sourceContainer?.rid}
|
|
||||||
styles={chooseSourceContainerStyles()}
|
|
||||||
style={chooseSourceContainerStyle()}
|
|
||||||
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
|
||||||
/>
|
|
||||||
<Separator className="panelSeparator" />
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
View container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
id="materializedViewId"
|
|
||||||
type="text"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
|
||||||
placeholder={`e.g., viewByEmailId`}
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
value={materializedViewId}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setMaterializedViewId(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Materialized View Definition
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={
|
|
||||||
<Link
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
|
||||||
target="blank"
|
|
||||||
>
|
|
||||||
Learn more about defining materialized views.
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
id="materializedViewDefinition"
|
|
||||||
type="text"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={"SELECT c.email, c.accountId FROM c"}
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
value={definition || ""}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
|
||||||
/>
|
|
||||||
<AddMVPartitionKeyComponent
|
|
||||||
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
|
||||||
/>
|
|
||||||
<AddMVThroughputComponent
|
|
||||||
{...{
|
|
||||||
enableDedicatedThroughput,
|
|
||||||
setEnabledDedicatedThroughput,
|
|
||||||
isSelectedSourceContainerSharedThroughput,
|
|
||||||
showCollectionThroughputInput,
|
|
||||||
materializedViewThroughputOnChange,
|
|
||||||
isMaterializedViewAutoscaleOnChange,
|
|
||||||
setIsThroughputCapExceeded,
|
|
||||||
isCostAknowledgedOnChange,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AddMVUniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
|
||||||
<AddMVAnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
|
||||||
)}
|
|
||||||
{showVectorSearchParameters() && (
|
|
||||||
<AddMVVectorSearchComponent
|
|
||||||
{...{
|
|
||||||
vectorEmbeddingPolicy,
|
|
||||||
setVectorEmbeddingPolicy,
|
|
||||||
vectorIndexingPolicy,
|
|
||||||
setVectorIndexingPolicy,
|
|
||||||
vectorPolicyValidated,
|
|
||||||
setVectorPolicyValidated,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showFullTextSearchParameters() && (
|
|
||||||
<AddMVFullTextSearchComponent
|
|
||||||
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AddMVAdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
|
||||||
{isExecuting && <PanelLoadingScreen />}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
|
|
||||||
import { CSSProperties } from "react";
|
|
||||||
|
|
||||||
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
|
|
||||||
return {
|
|
||||||
title: { height: 27, lineHeight: 27 },
|
|
||||||
dropdownItem: { fontSize: 12 },
|
|
||||||
dropdownItemDisabled: { fontSize: 12 },
|
|
||||||
dropdownItemSelected: { fontSize: 12 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chooseSourceContainerStyle(): CSSProperties {
|
|
||||||
return { width: 300, fontSize: 12 };
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AddMaterializedViewPanel render default panel 1`] = `
|
|
||||||
<form
|
|
||||||
className="panelFormWrapper"
|
|
||||||
id="panelMaterializedView"
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="panelMainContent"
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mandatoryStar"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
<Text
|
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
Source container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Dropdown
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder="Choose source container"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"fontSize": 12,
|
|
||||||
"width": 300,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styles={
|
|
||||||
{
|
|
||||||
"dropdownItem": {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
"dropdownItemDisabled": {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
"dropdownItemSelected": {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"height": 27,
|
|
||||||
"lineHeight": 27,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Separator
|
|
||||||
className="panelSeparator"
|
|
||||||
/>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mandatoryStar"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
<Text
|
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
View container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
aria-required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
className="panelTextField"
|
|
||||||
id="materializedViewId"
|
|
||||||
onChange={[Function]}
|
|
||||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
|
||||||
placeholder="e.g., viewByEmailId"
|
|
||||||
required={true}
|
|
||||||
size={40}
|
|
||||||
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mandatoryStar"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
<Text
|
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
Materialized View Definition
|
|
||||||
</Text>
|
|
||||||
<StyledTooltipHostBase
|
|
||||||
content={
|
|
||||||
<StyledLinkBase
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
|
||||||
target="blank"
|
|
||||||
>
|
|
||||||
Learn more about defining materialized views.
|
|
||||||
</StyledLinkBase>
|
|
||||||
}
|
|
||||||
directionalHint={4}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className="panelInfoIcon"
|
|
||||||
iconName="Info"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
</StyledTooltipHostBase>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
aria-required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
className="panelTextField"
|
|
||||||
id="materializedViewDefinition"
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder="SELECT c.email, c.accountId FROM c"
|
|
||||||
required={true}
|
|
||||||
size={40}
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<AddMVPartitionKeyComponent
|
|
||||||
partitionKey=""
|
|
||||||
setPartitionKey={[Function]}
|
|
||||||
setSubPartitionKeys={[Function]}
|
|
||||||
subPartitionKeys={[]}
|
|
||||||
/>
|
|
||||||
<AddMVThroughputComponent
|
|
||||||
isCostAknowledgedOnChange={[Function]}
|
|
||||||
isMaterializedViewAutoscaleOnChange={[Function]}
|
|
||||||
isSelectedSourceContainerSharedThroughput={[Function]}
|
|
||||||
materializedViewThroughputOnChange={[Function]}
|
|
||||||
setEnabledDedicatedThroughput={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
showCollectionThroughputInput={[Function]}
|
|
||||||
/>
|
|
||||||
<AddMVUniqueKeysComponent
|
|
||||||
setUniqueKeys={[Function]}
|
|
||||||
uniqueKeys={[]}
|
|
||||||
/>
|
|
||||||
<AddMVAnalyticalStoreComponent
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setEnableAnalyticalStore={[Function]}
|
|
||||||
/>
|
|
||||||
<AddMVAdvancedComponent
|
|
||||||
setSubPartitionKeys={[Function]}
|
|
||||||
setUseHashV1={[Function]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<PanelFooterComponent
|
|
||||||
buttonLabel="OK"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
@@ -94,7 +94,6 @@
|
|||||||
padding-left: @MediumSpace;
|
padding-left: @MediumSpace;
|
||||||
|
|
||||||
.paneErrorLink {
|
.paneErrorLink {
|
||||||
color: @LinkColor;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,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 { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
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";
|
||||||
@@ -175,26 +174,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 =
|
|
||||||
isDataplaneRbacSupported(userContext.apiType) &&
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -205,17 +193,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 (
|
||||||
@@ -225,6 +202,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
) {
|
) {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: true,
|
dataPlaneRbacEnabled: true,
|
||||||
|
hasDataPlaneRbacSettingChanged: true,
|
||||||
});
|
});
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
try {
|
try {
|
||||||
@@ -248,6 +226,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
} else {
|
} else {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
dataPlaneRbacEnabled: false,
|
dataPlaneRbacEnabled: false,
|
||||||
|
hasDataPlaneRbacSettingChanged: true,
|
||||||
});
|
});
|
||||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
||||||
@@ -503,7 +482,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>
|
||||||
@@ -553,37 +532,40 @@ 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.
|
<div className={styles.settingsSectionDescription}>
|
||||||
<a
|
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
ID RBAC.
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
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>
|
{" "}
|
||||||
|
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>
|
||||||
@@ -681,103 +663,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>
|
||||||
@@ -803,6 +784,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowParallelismOption && (
|
{shouldShowParallelismOption && (
|
||||||
<AccordionItem value="9">
|
<AccordionItem value="9">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -836,6 +818,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowPriorityLevelOption && (
|
{shouldShowPriorityLevelOption && (
|
||||||
<AccordionItem value="10">
|
<AccordionItem value="10">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -859,6 +842,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowGraphAutoVizOption && (
|
{shouldShowGraphAutoVizOption && (
|
||||||
<AccordionItem value="11">
|
<AccordionItem value="11">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -880,6 +864,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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
|
|||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||||
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user